oTree Hub Studio Heroku Forum Public projects Featured Example code Account Example code This page contains examples of how various oTree functions should be used.For example, search this page for before_next_page or after_all_players_arrive. gbat_fallback_solo_task_part2 / SoloTask.html From otree - snippets {{block title}} Single - player task {{endblock}} {{block content}} < p > < i > Here you can put a single - player task.... < / i > < / p > {{endblock}} gbat_fallback_solo_task_part2 / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'gbat_fallback_solo_task_part2' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class SoloTask(Page): pass page_sequence = [SoloTask] multi_page_timeout / Page1.html From otree - snippets {{block title}} Page 1 {{endblock}} {{block content}} < p > Page content goes here... < / p > {{next_button}} {{endblock}} multi_page_timeout / Page2.html From otree - snippets {{block title}} Page 2 {{endblock}} {{block content}} < p > Page content goes here... < / p > {{next_button}} {{endblock}} multi_page_timeout / Page3.html From otree - snippets {{block title}} Page 3 {{endblock}} {{block content}} < p > Page content goes here... < / p > {{next_button}} {{endblock}} multi_page_timeout / __init__.py From otree - snippets from otree.api import * doc = """ Timeout spanning multiple pages """ class C(BaseConstants): NAME_IN_URL = 'multi_page_timeout' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 TIMER_TEXT = "Time to complete this section:" class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass def get_timeout_seconds1(player: Player): participant = player.participant import time return participant.expiry - time.time() def is_displayed1(player: Player): """only returns True if there is time left.""" return get_timeout_seconds1(player) > 0 class Intro(Page): @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant import time participant.expiry = time.time() + 60 class Page1(Page): is_displayed = is_displayed1 get_timeout_seconds = get_timeout_seconds1 timer_text = C.TIMER_TEXT class Page2(Page): is_displayed = is_displayed1 get_timeout_seconds = get_timeout_seconds1 timer_text = C.TIMER_TEXT class Page3(Page): is_displayed = is_displayed1 timer_text = C.TIMER_TEXT get_timeout_seconds = get_timeout_seconds1 page_sequence = [Intro, Page1, Page2, Page3] multi_page_timeout / Intro.html From otree - snippets {{block title}} Introduction {{endblock}} {{block content}} < p > Press next to start the timer... < / p > {{next_button}} {{endblock}} custom_export_groups / __init__.py From otree - snippets from otree.api import * import random doc = """ custom_export: 1 row for each group """ class C(BaseConstants): NAME_IN_URL = 'custom_export' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): """generate some fake data for the export""" for g in subsession.get_groups(): g.gfield1 = random.randint(0, 100) class Group(BaseGroup): gfield1 = models.IntegerField() class Player(BasePlayer): pass # PAGES class MyPage(Page): pass page_sequence = [MyPage] def get_groups(players): """gets all groups that these players belong to, without duplicates""" already_added = set() groups = [] for p in players: group = p.group if group.id not in already_added: already_added.add(group.id) groups.append(group) return groups def custom_export(players): """ Export 1 row for each group """ yield ['session.code', 'round_number', 'group.id_in_subsession', 'group.gfield1'] for g in get_groups(players): yield [g.session.code, g.round_number, g.id_in_subsession, g.gfield1] custom_export_groups / Results.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} {{next_button}} {{endblock}} custom_export_groups / MyPage.html From otree - snippets {{block content}} < p > Go to the data export page.The downloaded file will have 1 row per group. < / p > {{endblock}} live_volunteer / __init__.py From otree - snippets from otree.api import * doc = """ Live volunteer's dilemma (first player to click moves everyone forward). """ class C(BaseConstants): NAME_IN_URL = 'live_volunteer' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 REWARD = cu(1000) VOLUNTEER_COST = cu(500) class Subsession(BaseSubsession): pass class Group(BaseGroup): has_volunteer = models.BooleanField(initial=False) class Player(BasePlayer): is_volunteer = models.BooleanField() volunteer_id = models.IntegerField() # PAGES class MyPage(Page): @staticmethod def is_displayed(player: Player): group = player.group return not group.has_volunteer @staticmethod def live_method(player: Player, data): group = player.group # print('data is', data) if group.has_volunteer: return if data.get('volunteer'): group.has_volunteer = True # mark all other players as non-volunteers for p in player.get_others_in_group(): p.payoff = C.REWARD p.is_volunteer = False # mark myself as a volunteer player.is_volunteer = True player.payoff = C.REWARD - C.VOLUNTEER_COST # broadcast to the group that the game is finished. return {0: dict(finished=True)} @staticmethod def error_message(player: Player, values): """Prevent users from proceeding before someone has volunteered.""" group = player.group if not group.has_volunteer: return "Can't move forward" class Results(Page): pass page_sequence = [MyPage, Results] live_volunteer / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} {{ if player.is_volunteer}} < p > You volunteered. < / p > {{ else}} < p > Someone else volunteered. < / p > {{endif}} < p > Your payoff is therefore {{player.payoff}}. < / p > {{next_button}} {{endblock}} live_volunteer / MyPage.html From otree - snippets {{block content}} < p > This is a volunteer dilemma with {{C.PLAYERS_PER_GROUP}} players per group. If someone in the group volunteers, each player will get a reward of {{C.REWARD}}. But the volunteer will pay a penalty of {{C.VOLUNTEER_COST}}. < / p > < button type = "button" class ="btn btn-primary" onclick="sendVolunteer()" > I volunteer < / button > < br > < br > < p > Here you can chat with your group.< / p > {{chat}} < script > function sendVolunteer() { liveSend({'volunteer': true}); } function liveRecv(data) { if (data.finished) { document.getElementById('form').submit(); } } document.addEventListener('DOMContentLoaded', (event) = > { liveSend({}); }); < / script > {{endblock}} css / __init__.py From otree - snippets from otree.api import * doc = """ Using CSS to style timer and chat box. """ class C(BaseConstants): NAME_IN_URL = 'css' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): timeout_seconds = 30 * 60 page_sequence = [MyPage] css / MyPage.html From otree - snippets {{block title}} Demo of custom styles {{endblock}} {{block content}} < style > .otree - timer { position: sticky; top: 0 px; } .chat - widget { position: fixed; bottom: 0 px; right: 0 px; max - width: 50 em; z - index: 100; background - color: # eee; padding: 1 em; } < / style > < p > Sticky timer and chat box in bottom - right corner. < / p > < p > Sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text < / p > < p > Sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text < / p > < p > Sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text < / p > < p > Sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text < / p > < p > Sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text < / p > < p > Sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text sample text < / p > < div class ="chat-widget" > < b > Chat with your group < / b > {{chat}} < / div > {{endblock}} audio_alert / __init__.py From otree - snippets from otree.api import * doc = """ Audio alert (speak some text to get the participant's attention, after a wait page) """ class C(BaseConstants): NAME_IN_URL = 'audio_alert' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 3 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class MyPage(Page): pass class GBAT(WaitPage): pass class Results(Page): pass page_sequence = [MyPage, GBAT, Results] audio_alert / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < script > function sayReady() { let msg = new SpeechSynthesisUtterance(); // or: de - DE, zh - CN, ja - JP, es - MX, etc. msg.language = 'en-US'; // actually better to use js_vars than double - braces msg.text = "Ready player {{ player.id_in_group }}"; window.speechSynthesis.speak(msg); } if (document.hidden) { sayReady(); } < / script > < p > < i > You should hear a voice saying the game is ready, if the user is in another tab when this page loads. < / i > < / p > {{next_button}} {{endblock}} audio_alert / MyPage.html From otree - snippets {{block title}} Game {{endblock}} {{block content}} < p > < i > Your game goes here... < / i > < / p > {{formfields}} {{next_button}} {{endblock}} balance_treatments_for_dropouts / __init__.py From otree - snippets from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'balance_treatments_for_dropouts' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 TREATMENTS = ['red', 'blue', 'green'] class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): session = subsession.session session.completions_by_treatment = {color: 0 for color in C.TREATMENTS} class Group(BaseGroup): pass class Player(BasePlayer): color = models.StringField() # PAGES class Intro(Page): @staticmethod def before_next_page(player: Player, timeout_happened): session = player.session player.color = min( C.TREATMENTS, key=lambda color: session.completions_by_treatment[color], ) class Task(Page): @staticmethod def before_next_page(player: Player, timeout_happened): session = player.session session.completions_by_treatment[player.color] += 1 page_sequence = [Intro, Task] gbat_new_partners / __init__.py From otree - snippets from otree.api import * doc = """ group by arrival time, but in each round assign to a new partner. """ class C(BaseConstants): NAME_IN_URL = 'gbat_new_partners' PLAYERS_PER_GROUP = None NUM_ROUNDS = 3 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): session = subsession.session session.past_groups = [] def group_by_arrival_time_method(subsession: Subsession, waiting_players): session = subsession.session import itertools # this generates all possible pairs of waiting players # and checks if the group would be valid. for possible_group in itertools.combinations(waiting_players, 2): # use a set, so that we can easily compare even if order is different # e.g. {1, 2} == {2, 1} pair_ids = set(p.id_in_subsession for p in possible_group) # if this pair of players has not already been played if pair_ids not in session.past_groups: # mark this group as used, so we don't repeat it in the next round. session.past_groups.append(pair_ids) # in this function, # 'return' means we are creating a new group with this selected pair return possible_group class Group(BaseGroup): pass class Player(BasePlayer): pass class ResultsWaitPage(WaitPage): group_by_arrival_time = True body_text = "Waiting to pair you with someone you haven't already played with" class MyPage(Page): @staticmethod def vars_for_template(player: Player): return dict(partner=player.get_others_in_group()[0]) page_sequence = [ResultsWaitPage, MyPage] gbat_new_partners / MyPage.html From otree - snippets {{block title}} Round {{subsession.round_number}} {{endblock}} {{block content}} < p > < i > This game uses group_by_arrival_time for multiple rounds. In each round, we ensure that you are matched with a different player. < / i > < / p > < p > Your partner is player {{partner.id_in_subsession}}. < / p > < p > Here are the pairs that have already played together: {{session.past_groups}} < / p > {{next_button}} {{endblock}} gbat_treatments / __init__.py From otree - snippets from otree.api import * doc = """ Conventionally, group-level treatments are assigned in creating_session: for g in subsession.get_groups(): g.treatment = random.choice([True, False]) However, this doesn't work when using group_by_arrival_time, because groups are not determined until players arrive at the wait page. (All players are in the same group initially.) Instead, you need to assign treatments in after_all_players_arrive. """ class C(BaseConstants): NAME_IN_URL = 'gbat_treatments' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): treatment = models.BooleanField() class Player(BasePlayer): pass class GBATWaitPage(WaitPage): group_by_arrival_time = True @staticmethod def after_all_players_arrive(group: Group): import random group.treatment = random.choice([True, False]) class MyPage(Page): pass page_sequence = [GBATWaitPage, MyPage] balance_treatments_for_dropouts / Task.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > You were assigned to the < b > {{player.color}} < / b > treatment, because this treatment group currently was completed by the fewest number of players. < / p > {{next_button}} {{endblock}} balance_treatments_for_dropouts / Intro.html From otree - snippets {{block title}} {{endblock}} {{block content}} Welcome {{next_button}} {{endblock}} random_num_rounds_multiplayer / __init__.py From otree - snippets from otree.api import * doc = """ Random number of rounds for multiplayer (random stopping rule) """ class C(BaseConstants): NAME_IN_URL = 'random_num_rounds_multiplayer' PLAYERS_PER_GROUP = None # choose NUM_ROUNDS high enough that the chance of # maxing out is negligible NUM_ROUNDS = 50 STOPPING_PROBABILITY = 0.2 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): p.participant.finished_rounds = False class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): pass class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): import random if random.random() < C.STOPPING_PROBABILITY: print('ending game') for p in group.get_players(): p.participant.finished_rounds = True # your usual after_all_players_arrive goes here... class Results(Page): @staticmethod def app_after_this_page(player: Player, upcoming_apps): participant = player.participant if participant.finished_rounds: return upcoming_apps[0] page_sequence = [MyPage, ResultsWaitPage, Results] random_num_rounds_multiplayer / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > < i > Your results page goes here... < / i > < / p > {{ if participant.finished_rounds}} < p > The game was randomly determined to stop at this round. < / p > {{ else}} < p > The game will continue to the next round. < / p > {{endif}} {{formfields}} {{next_button}} {{endblock}} random_num_rounds_multiplayer / MyPage.html From otree - snippets {{block title}} Game {{endblock}} {{block content}} < p > This game uses a random stopping rule.After each round, the game has a probability of {{C.STOPPING_PROBABILITY}} of being stopped. < / p > < p > < i > Your game goes here... < / i > < / p > {{formfields}} {{next_button}} {{endblock}} timer_custom / __init__.py From otree - snippets from otree.api import * doc = """ Timer: replacing the default timer with your own """ class C(BaseConstants): NAME_IN_URL = 'timer_custom' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): timeout_seconds = 60 page_sequence = [MyPage] timer_custom / MyPage.html From otree - snippets {{block title}} Custom timer element {{endblock}} {{block content}} < p > You have < span id = "time-left" > < / span > seconds left. < / p > {{formfields}} {{next_button}} < script > let customTimerEle = document.getElementById('time-left'); document.addEventListener("DOMContentLoaded", function(event) { $('.otree-timer__time-left').on('update.countdown', function(event) { customTimerEle.innerText = event.offset.totalSeconds; }); }); < / script > {{endblock}} questions_from_csv_simple / __init__.py From otree - snippets from otree.api import * doc = """ Read quiz questions from a CSV (simple version). See also the 'complex' version of this app. """ def read_csv(): import csv f = open(__name__ + '/stimuli.csv', encoding='utf-8-sig') rows = list(csv.DictReader(f)) return rows class C(BaseConstants): NAME_IN_URL = 'questions_from_csv_simple' PLAYERS_PER_GROUP = None QUESTIONS = read_csv() NUM_ROUNDS = len(QUESTIONS) class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): current_question = C.QUESTIONS[subsession.round_number - 1] for p in subsession.get_players(): p.question = current_question['question'] p.optionA = current_question['optionA'] p.optionB = current_question['optionB'] p.optionC = current_question['optionC'] p.solution = current_question['solution'] p.participant.quiz_num_correct = 0 class Group(BaseGroup): pass class Player(BasePlayer): question = models.StringField() optionA = models.StringField() optionB = models.StringField() optionC = models.StringField() solution = models.StringField() choice = models.StringField(widget=widgets.RadioSelect) is_correct = models.BooleanField() def choice_choices(player: Player): return [ ['A', player.optionA], ['B', player.optionB], ['C', player.optionC], ] class Stimuli(Page): form_model = 'player' form_fields = ['choice'] @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant player.is_correct = player.choice == player.solution participant.quiz_num_correct += int(player.is_correct) class Results(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): return dict(round_players=player.in_all_rounds()) page_sequence = [Stimuli, Results] questions_from_csv_simple / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > You gave {{participant.quiz_num_correct}} correct answers. < / p > < table class ="table" > < tr > < th > question < / th > < th > optionA < / th > < th > optionB < / th > < th > optionC < / th > < th > Your choice < / th > < th > solution < / th > < th > correct? < / th > < / tr > {{ for p in round_players}} < tr > < td > {{p.question}} < / td > < td > {{p.optionA}} < / td > < td > {{p.optionB}} < / td > < td > {{p.optionC}} < / td > < td > {{p.choice}} < / td > < td > {{p.solution}} < / td > < td > {{p.is_correct}} < / td > < / tr > {{endfor}} < / table > {{endblock}} pay_random_app_single_player / __init__.py From otree - snippets from otree.api import * doc = """ """ class C(BaseConstants): NAME_IN_URL = 'pay_random_app2' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): # initialize an empty dict to store how much they made in each app p.participant.app_payoffs = {} class Group(BaseGroup): pass class Player(BasePlayer): potential_payoff = models.CurrencyField() # PAGES class MyPage(Page): @staticmethod def before_next_page(player: Player, timeout_happened): """in a single-player game you typically set payoff in before_next_page, so that's what we demonstrate here. """ participant = player.participant import random potential_payoff = random.randint(100, 200) player.potential_payoff = potential_payoff # this is designed for apps that have a single round. # if your app has multiple rounds, see the pay_random_round app. participant.app_payoffs[__name__] = potential_payoff class Results(Page): pass page_sequence = [MyPage, Results] pay_random_app_single_player / Results.html From otree - snippets {{block title}} App 2 Results {{endblock}} {{block content}} < p > Your payoff in this app is {{player.potential_payoff}}. < / p > {{next_button}} {{endblock}} pay_random_app_single_player / MyPage.html From otree - snippets {{block title}} App 2 {{endblock}} {{block content}} < p > < i > Your game would normally go here.In this case, your payoff will be determined randomly. < / i > < / p > {{next_button}} {{endblock}} back_button / Task.html From otree - snippets {{block title}} Task {{endblock}} {{block content}} < p > < i > Experiment goes here... < / i > < / p > {{next_button}} {{endblock}} back_button / Instructions.html From otree - snippets {{block title}} Instructions {{endblock}} {{block content}} < style > .tab { display: none; } < / style > {{include_sibling 'tabs.html'}} < script > let activeTab = 0; let tabs = document.getElementsByClassName('tab'); function showCurrentTabOnly() { for (let i = 0; i < tabs.length; i++) { let tab = tabs[i]; if (i == = activeTab) { tab.style.display = 'block'; tab.scrollIntoView(); } else { tab.style.display = 'none'; } } } showCurrentTabOnly(); for (let btn of document.getElementsByClassName('btn-tab')) { btn.onclick = function () { activeTab += parseInt(btn.dataset.offset); showCurrentTabOnly(); } } < / script > {{endblock}} questions_from_csv_simple / Stimuli.html From otree - snippets {{block title}} Question {{player.round_number}} of {{C.NUM_ROUNDS}} {{endblock}} {{block content}} < p > < b > {{player.question}} < / b > < / p > {{formfields}} {{next_button}} {{endblock}} gbat_fallback_solo_task_part0 / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'gbat_fallback_solo_task_part0' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class MyPage(Page): @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant import time participant.wait_page_arrival = time.time() page_sequence = [MyPage] gbat_fallback_solo_task_part0 / MyPage.html From otree - snippets {{block title}} Welcome {{endblock}} {{block content}} < p > Welcome! Please press next. You will be grouped with another participant. If we cannot group you with another participant within a minute, you will proceed to a single - player task. < / p > {{next_button}} {{endblock}} back_button / __init__.py From otree - snippets from otree.api import * doc = """ Back button for multiple instructions pages """ class C(BaseConstants): NAME_IN_URL = 'back_button' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class Instructions(Page): pass class Task(Page): pass page_sequence = [Instructions, Task] back_button / tabs.html From otree - snippets < div class ="tab" > Instructions first page content goes here... Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum < p > < button type = "button" class ="btn-tab" data-offset="1" > Next < / button > < / p > < / div > < div class ="tab" > < !-- Duplicate this div if you have more than 3 pages of instructions... --> Instructions middle page content goes here. Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum Lorem ipsum lorem ipsum < p > < button type = "button" class ="btn-tab" data-offset="-1" > Previous < / button > < button type = "button" class ="btn-tab" data-offset="1" > Next < / button > < / p > < / div > < div class ="tab" > Instructions last page content goes here... < p > < button type = "button" class ="btn-tab" data-offset="-1" > Previous < / button > < button class ="btn btn-primary" > Next < / button > < / p > < / div > other_player_previous_rounds / __init__.py From otree - snippets from otree.api import * doc = """ Showing other players' decisions from previous rounds """ class C(BaseConstants): NAME_IN_URL = 'other_player_previous_rounds' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 5 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): import random subsession.group_randomly() # for demo purposes we just generate random data. # of course in a real game, there would be a formfield where a user # enters their contribution for player in subsession.get_players(): player.contribution = random.randint(0, 99) class Group(BaseGroup): pass class Player(BasePlayer): contribution = models.IntegerField() def get_partner(player: Player): return player.get_others_in_group()[0] # PAGES class MyPage(Page): @staticmethod def vars_for_template(player: Player): partner = get_partner(player) my_partner_previous = partner.in_all_rounds() my_previous_partners = [ get_partner(me_prev) for me_prev in player.in_all_rounds() ] return dict( partner=partner, my_partner_previous=my_partner_previous, my_previous_partners=my_previous_partners, ) page_sequence = [MyPage] other_player_previous_rounds / MyPage.html From otree - snippets {{block title}} Round {{subsession.round_number}} {{endblock}} {{block content}} < p > < i > This app shows how you can chain methods like < code >.in_all_rounds() < / code > with < code > group.get_players() < / code >, < code > player.get_others_in_group() < / code >, etc, to get the history of other players in different ways. < / i > < / p > < h3 > My current partner 's history < p > My current partner: player {{partner.id_in_subsession}} < / p > < table class ="table" > < tr > < th > Round < / th > < th > contribution < / th > < / tr > {{ for p in my_partner_previous}} < tr > < td > {{p.round_number}} < / td > < td > {{p.contribution}} < / td > < / tr > {{endfor}} < / table > < h3 > History of my partners < / h3 > < table class ="table" > < tr > < th > Round < / th > < th > Player < / th > < th > contribution < / th > < / tr > {{ for p in my_previous_partners}} < tr > < td > {{p.round_number}} < / td > < td > {{p.id_in_subsession}} < / td > < td > {{p.contribution}} < / td > < / tr > {{endfor}} < / table > {{next_button}} {{endblock}} slider_live_label / __init__.py From otree - snippets from otree.api import * doc = """ Slider with live updating label """ class C(BaseConstants): NAME_IN_URL = 'slider_live_label' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 ENDOWMENT = 100 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): give = models.IntegerField( min=0, max=C.ENDOWMENT, label="How much do you want to give?" ) # PAGES class MyPage(Page): form_model = 'player' form_fields = ['give'] @staticmethod def js_vars(player: Player): return dict(endowment=C.ENDOWMENT) page_sequence = [MyPage] dropout_detection / Page1.html From otree - snippets {{block content}} < p > You need to submit this page before the timeout occurs. Otherwise you will be considered a dropout. < / p > < p > < i > You can put formfields on this page etc. < / i > < / p > {{next_button}} {{endblock}} dropout_detection / Page2.html From otree - snippets {{block content}} < p > < i > Experiment continues here... < / i > < / p > {{next_button}} {{endblock}} chat_with_experimenter / MyPage.html From otree - snippets {{block title}} Chat with experimenter {{endblock}} {{block content}} < p > In the bottom right corner of the screen, there is a button to start a chat with the experimenter. < / p > < p > < i > In this demo, these messages are currently being sent to oTree.org 's Papercups server. To set up your own server, See < a href = "https://otree.readthedocs.io/en/latest/admin.html#experimenter-chat" > here < / a >. < / i > < / p > { # you should put this 'include' on every page that needs a chat widget #} {{include_sibling 'papercups.html'}} {{endblock}} chat_with_experimenter / __init__.py From otree - snippets from otree.api import * doc = """ Chat with experimenter, using Papercups """ class C(BaseConstants): NAME_IN_URL = 'chat_with_experimenter' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): pass page_sequence = [MyPage] experimenter_input / Intro.html From otree - snippets {{block content}} < p > < i > Put the first part of your game here... < / i > < / p > {{next_button}} {{endblock}} experimenter_input / MyPage.html From otree - snippets {{block content}} < p > The number drawn by the experimenter was {{group.exp_input}}. < / p > < p > < i > Your game can continue here.... < / i > < / p > {{endblock}} dropout_detection / __init__.py From otree - snippets from otree.api import * doc = """ Dropout detection (if user does not submit page in time) """ class C(BaseConstants): NAME_IN_URL = 'detect_dropout' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): is_dropout = models.BooleanField(initial=False) class Page1(Page): timeout_seconds = 10 @staticmethod def before_next_page(player: Player, timeout_happened): # note: bugfix if timeout_happened: player.is_dropout = True class ByeDropout(Page): @staticmethod def is_displayed(player: Player): return player.is_dropout @staticmethod def error_message(player: Player, values): return "Cannot proceed past this page" class Page2(Page): pass page_sequence = [Page1, ByeDropout, Page2] dropout_detection / ByeDropout.html From otree - snippets {{block title}} End {{endblock}} {{block content}} Sorry, you did not submit the page in time. The experiment is now finished. {{endblock}} experimenter_input / __init__.py From otree - snippets from otree.api import * doc = """ Experimenter input during the experiment, e.g. entering the result of a random draw. If you want the experimenter to be able to make an input at any time, you can use the REST API (especially the session_vars endpoint). """ class C(BaseConstants): NAME_IN_URL = 'experimenter_input' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 PASSWORD = 'mypass' class Subsession(BaseSubsession): pass class Group(BaseGroup): exp_input = models.IntegerField() has_exp_input = models.BooleanField(initial=False) class Player(BasePlayer): pass # PAGES class Intro(Page): pass class ExpInput(Page): """ It should be a live page so that you can notify all other players to advance """ @staticmethod def live_method(player: Player, data): group = player.group if ('exp_input' in data) and ('password' in data): if data['password'] != C.PASSWORD: return {player.id_in_group: dict(error="Incorrect password")} group.exp_input = data['exp_input'] group.has_exp_input = True # broadcast to the whole group whether the game is finished return {0: dict(finished=group.has_exp_input)} @staticmethod def error_message(player: Player, values): group = player.group if not group.has_exp_input: return "Experimenter has not input data yet" class MyPage(Page): pass page_sequence = [Intro, ExpInput, MyPage] experimenter_input / ExpInput.html From otree - snippets {{block title}} Random draw {{endblock}} {{block content}} < p > Please wait.The experimenter will draw a random number. < / p > < details > < summary > If you are the experimenter, click here. < / summary > < label class ="col-form-label" > Number drawn < input type = "number" class ="form-control" id="exp_input" > < / label > < br > < label class ="col-form-label" > Password { # you can add type="password" for a proper password input #} < input class ="form-control" id="password" > < / label > < br > < button type = "button" onclick = "sendData()" > Submit < / button > < p > < small > Hint for demo purposes: password is "{{ C.PASSWORD }}". You can get to this page by opening the participant 's start URL. < / small > < / p > < / details > < script > let expInput = document.getElementById('exp_input'); let passwordInput = document.getElementById('password'); function sendData() { liveSend({'exp_input': parseInt(expInput.value), password: passwordInput.value}); } function liveRecv(data) { if (data.finished) { document.getElementById('form').submit(); } if (data.error) { alert(data.error); } } document.addEventListener("DOMContentLoaded", function(event) { // need this so that you proceed even if you arrive late or got disconnected liveSend({}); }); < / script > {{endblock}} multi_language / __init__.py From otree - snippets import random from otree.api import * from settings import LANGUAGE_CODE doc = """ How to translate an app to multiple languages (e.g. English and German). There are 2 ways to define localizable strings: (1) Put it in a "lexicon" file (see lexicon_en.py, lexicon_de.py). This is the easiest technique, and it allows you to easily reuse the same string multiple times. (2) If the string contains variables, then it should to be defined in the template. Use an if-statement, like {{ if de }} Nein {{ else }} No {{ endif }} When you change the LANGUAGE_CODE in settings.py, the language will automatically be changed. Note: this technique does not require .po files, which are a more complex technique. """ if LANGUAGE_CODE == 'de': from .lexicon_de import Lexicon else: from .lexicon_en import Lexicon # this is the dict you should pass to each page in vars_for_template, # enabling you to do if-statements like {{ if de }} Nein {{ else }} No {{ endif }} which_language = {'en': False, 'de': False, 'zh': False} # noqa which_language[LANGUAGE_CODE[:2]] = True class C(BaseConstants): NAME_IN_URL = 'bret' NUM_ROUNDS = 1 PLAYERS_PER_GROUP = None class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): boxes_collected = models.IntegerField(label=Lexicon.boxes_collected) class Game(Page): form_model = 'player' form_fields = ['boxes_collected'] @staticmethod def vars_for_template(player: Player): return dict(Lexicon=Lexicon, **which_language) class Results(Page): @staticmethod def vars_for_template(player: Player): # this is just fake data return dict( boxes_total=64, bomb_row=2, bomb_col=1, bomb=True, payoff=player.payoff, box_value=cu(5), boxes_collected=player.boxes_collected, Lexicon=Lexicon, **which_language ) page_sequence = [Game, Results] multi_language / Results.html From otree - snippets {{block title}} {{Lexicon.results}} {{endblock}} {{block content}} {{ if de}} Sie haben sich entschieden {{boxes_collected}} von {{boxes_total}} Boxen zu sammeln. {{ else}} You chose to collect {{boxes_collected}} out of {{boxes_total}} boxes. {{endif}} < p > {{ if de}} Die Bombe war hinter der Box in Reihe {{bomb_row}}, Spalte {{bomb_col}} versteckt. {{ else}} The bomb was hidden behind the box in row {{bomb_row}}, column {{bomb_col}}. {{endif}} < / p > < p > {{ if bomb}} {{ if de}} Die Bombe befand sich unter den von Ihnen gesammelten {{boxes_collected}} Boxen. < br / > Entsprechend wurden alle Ihre gesammelten Erträge zerstört und Ihre Auszahlung in dieser Aufgabe beträgt {{player.payoff}}. {{ else}} The bomb was among your {{boxes_collected}} collected boxes. < br / > Accordingly, all your earnings in this task were destroyed and your payoff amounts to {{player.payoff}}. {{endif}} {{ else}} {{ if de}} Die Bombe war nicht unter den von Ihnen eingesammelten Boxen. < br / > Dementsprechend erhalten Sie {{box_value}} für jede der {{boxes_collected}} Boxen, sodass sich Ihre Auszahlung in dieser Aufgabe auf < b > {{player.payoff}} < / b > beläuft. {{ else}} Your collected boxes did not contain the bomb. < br / > Thus, you receive {{box_value}} for each of the {{boxes_collected}} boxes you collected such that your payoff from this task amounts to < b > {{player.payoff}} < / b >. {{endif}} {{endif}} < / p > {{next_button}} {{endblock}} multi_language / Game.html From otree - snippets {{block title}} {{Lexicon.your_decision}} {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} gbat_fallback_smaller_group_part1 / __init__.py From otree - snippets from otree.api import * doc = """ group_by_arrival_time: fall back to a smaller group if not enough people show up """ class C(BaseConstants): NAME_IN_URL = 'gbat_fallback_smaller_group_part1' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def waiting_seconds(player): import time participant = player.participant wait = int(time.time() - participant.wait_page_arrival) # print('Player', player.id_in_subsession, 'waiting for', wait, 'seconds') return wait def ranked_waiting_seconds(waiting_players): waits = [waiting_seconds(p) for p in waiting_players] waits.sort(reverse=True) return waits def group_by_arrival_time_method(subsession, waiting_players): # print("number of players waiting:", len(waiting_players)) # ideal case if len(waiting_players) >= 4: print("Creating a full sized group!") return waiting_players[:4] waits = ranked_waiting_seconds(waiting_players) if len(waits) == 3 and waits[2] > 60: print( "3 players have been waiting for longer than a minute, " "so we settle for a group of 3" ) return waiting_players if len(waits) >= 2 and waits[1] > 2 * 60: print( "2 players have been waiting for longer than 2 minutes, " "so we group whoever is available" ) return waiting_players # you can add your own additional rules based on waiting time and # number of waiting players class Group(BaseGroup): pass class Player(BasePlayer): favorite_color = models.StringField(label="What is your favorite color?") class GBAT(WaitPage): group_by_arrival_time = True class GroupTask(Page): form_model = 'player' form_fields = ['favorite_color'] class MyWait(WaitPage): pass class Results(Page): pass page_sequence = [GBAT, GroupTask, MyWait, Results] gbat_fallback_smaller_group_part1 / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > The colors chosen in your group were: < / p > < ul > {{ for p in group.get_players()}} < li > {{p.favorite_color}} < / li > {{endfor}} < / ul > {{next_button}} {{endblock}} gbat_fallback_smaller_group_part1 / GroupTask.html From otree - snippets {{block content}} < p > Your game goes here... < / p > {{formfields}} {{next_button}} {{endblock}} save_wrong_answers / Failed.html From otree - snippets {{block content}} Sorry, you gave too many wrong answers to the comprehension test. {{endblock}} save_wrong_answers / __init__.py From otree - snippets from otree.api import * doc = """ Store the history of invalid responses a user made. """ class C(BaseConstants): NAME_IN_URL = 'save_wrong_answers' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): quiz1 = models.IntegerField(label='What is 2 + 2?') quiz2 = models.StringField( label='What is the capital of Canada?', choices=['Ottawa', 'Toronto', 'Vancouver'], ) quiz3 = models.IntegerField(label="What year did COVID-19 start?") quiz4 = models.BooleanField(label="Is 4 a prime number") class IncorrectResponse(ExtraModel): player = models.Link(Player) field_name = models.StringField() response = models.StringField() class MyPage(Page): form_model = 'player' form_fields = ['quiz1', 'quiz2', 'quiz3', 'quiz4'] @staticmethod def error_message(player: Player, values): solutions = dict(quiz1=4, quiz2='Ottawa', quiz3=2019, quiz4=False) errors = {name: 'Try again' for name in solutions if values[name] != solutions[name]} if errors: for name in errors: response = values[name] IncorrectResponse.create(player=player, field_name=name, response=str(response)) return errors class Results(Page): pass page_sequence = [MyPage, Results] def custom_export(players): """For data export page""" yield ['participant_code', 'id_in_session', 'round_number', 'field_name', 'response'] responses = IncorrectResponse.filter() for resp in responses: player = resp.player participant = player.participant yield [participant.code, participant.id_in_session, player.round_number, resp.field_name, resp.response] save_wrong_answers / Results.html From otree - snippets {{block title}} Thank you {{endblock}} {{block content}} < p > You answered all questions correctly < / p > {{endblock}} save_wrong_answers / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} count_button_clicks / __init__.py From otree - snippets from otree.api import * doc = """Count button clicks""" class C(BaseConstants): NAME_IN_URL = 'count_button_clicks' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): button_clicks = models.IntegerField(initial=0) link_clicks = models.IntegerField(initial=0) # PAGES class MyPage(Page): @staticmethod def live_method(player: Player, data): if data == 'clicked-button': player.button_clicks += 1 if data == 'clicked-link': player.link_clicks += 1 class Results(Page): pass page_sequence = [MyPage, Results] count_button_clicks / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > You clicked the button {{player.button_clicks}} times and the link {{player.link_clicks}} times. < / p > {{endblock}} count_button_clicks / MyPage.html From otree - snippets {{block title}} Click the button {{endblock}} {{block content}} < p > This app records to the database the number of times you click the button or the link. < / p > < p > < button type = "button" onclick = "liveSend('clicked-button')" > Click me < / button > < a href = "https://wikipedia.org" onclick = "liveSend('clicked-link')" target = "_blank" > Click me < / a > < / p > {{next_button}} {{endblock}} slider_live_label / MyPage.html From otree - snippets {{block title}} {{endblock}} {{block content}} < p > Move the slider to decide how much to give. < / p > < input type = "range" name = "give" min = "0" max = "{{ C.ENDOWMENT }}" oninput = "updateDescription(this)" > < p id = "description" > < / p > < !-- by leaving the description blank initially, we prompt the user to move the slider, reducing the anchoring / default effect. --> < script > let description = document.getElementById('description'); function updateDescription(input) { let give = parseInt(input.value); let keep = js_vars.endowment - give; description.innerText = `Give ${give} points and keep ${keep} for yourself.` } < / script > {{next_button}} {{endblock}} constant_sum / __init__.py From otree - snippets from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'constant_sum' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): a = models.CurrencyField() b = models.CurrencyField() c = models.CurrencyField() # PAGES class MyPage(Page): form_model = 'player' form_fields = ['a', 'b', 'c'] @staticmethod def error_message(player: Player, values): # since 'values' is a dict, you could also do sum(values.values()) if values['a'] + values['b'] + values['c'] != 100: return 'Numbers must add up to 100' page_sequence = [MyPage] constant_sum / MyPage.html From otree - snippets {{block content}} < p > Please split your 100 points between A, B, and C. < / p > {{formfields}} < p > < b > Total: < span id = "total" > < / span > < / b > < / p > < script > let inputlist = document.getElementsByTagName('input'); let totalDisplay = document.getElementById('total'); function updateSum() { let total = 0; for (let field of inputlist) { total += parseInt(input.value | | 0); } totalDisplay.innerText = total; } for (let input of inputlist) { input.oninput = updateSum; } < / script > {{next_button}} {{endblock}} history_table / __init__.py From otree - snippets from otree.api import * doc = """History table""" class C(BaseConstants): NAME_IN_URL = 'history_table' PLAYERS_PER_GROUP = None NUM_ROUNDS = 10 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): number = models.IntegerField(label="Enter a number") class MyPage(Page): form_model = 'player' form_fields = ['number'] class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(me_in_all_rounds=player.in_all_rounds()) page_sequence = [MyPage, Results] history_table / Results.html From otree - snippets {{block title}} History {{endblock}} {{block content}} < table class ="table" > < tr > < th > Round < / th > < th > Number < / th > < / tr > {{ for p in me_in_all_rounds}} < tr > < td > {{p.round_number}} < / td > < td > {{p.number}} < / td > < / tr > {{endfor}} < / table > {{next_button}} {{endblock}} history_table / MyPage.html From otree - snippets {{block title}} Enter a number {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} sequential_symmetric / table.html From otree - snippets {{ if players}} < table class ="table" style="width: auto" > < tr > < th > Player < / th > < th > Guess < / th > < / tr > {{ for p in players}} < tr > < td > Player {{p.id_in_group}} < / td > < td > {{p.decision}} < / td > < / tr > {{endfor}} < / table > {{endif}} sequential_symmetric / __init__.py From otree - snippets from otree.api import * doc = """ Sequential / cascade game (symmetric). Also see "intergenerational" featured app. """ class C(BaseConstants): NAME_IN_URL = 'sequential_symmetric' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 MAIN_TEMPLATE = __name__ + '/Decide.html' FORM_FIELDS = ['decision'] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): decision = models.IntegerField( label="How many countries are there in Africa? (Make your best guess)" ) def vars_for_template1(player: Player): return dict( # get the players whose ID is less than mine players=[ p for p in player.get_others_in_group() if p.id_in_group < player.id_in_group ] ) # PAGES class P1(Page): form_model = 'player' form_fields = C.FORM_FIELDS template_name = C.MAIN_TEMPLATE @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 vars_for_template = vars_for_template1 class WaitPage1(WaitPage): pass class P2(Page): form_model = 'player' form_fields = C.FORM_FIELDS template_name = C.MAIN_TEMPLATE @staticmethod def is_displayed(player: Player): return player.id_in_group == 2 vars_for_template = vars_for_template1 class WaitPage2(WaitPage): pass class P3(Page): form_model = 'player' form_fields = C.FORM_FIELDS template_name = C.MAIN_TEMPLATE @staticmethod def is_displayed(player: Player): return player.id_in_group == 3 vars_for_template = vars_for_template1 class WaitPage3(WaitPage): pass class Results(Page): @staticmethod def vars_for_template(player: Player): group = player.group return dict(players=group.get_players()) page_sequence = [P1, WaitPage1, P2, WaitPage2, P3, WaitPage3, Results] sequential_symmetric / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > Here are the results.(You are player {{player.id_in_group}}.) < / p > {{include_sibling 'table.html'}} {{endblock}} sequential_symmetric / Decide.html From otree - snippets {{block content}} < ul > < li > This is a sequential game with {{C.PLAYERS_PER_GROUP}} players.< / li > < li > You are player {{player.id_in_group}}. < / li > < li > Each player will see the previous player 's choices < / ul > {{include_sibling 'table.html'}} {{formfields}} {{next_button}} {{endblock}} redirect_to_other_website / Redirect.html From otree - snippets {{block title}} Redirecting... {{endblock}} {{block content}} < script > window.location.href = js_vars.redirect_url; < / script > {{endblock}} redirect_to_other_website / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'redirect_to_other_website' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): citizenship = models.StringField() class MyPage(Page): form_model = 'player' form_fields = ['citizenship'] class Redirect(Page): @staticmethod def js_vars(player: Player): # google is just an example. you should change this to qualtrics or whatever survey provider # you are using. return dict(redirect_url='https://www.google.com/search?q=' + player.citizenship) page_sequence = [MyPage, Redirect] redirect_to_other_website / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} {{formfields}} < p > < i > After the user clicks 'next', they will be directed to another website. We append the user 's data to the URL, for example: < code > google.com / search?q = Canada < / code > < / i > < / p > {{next_button}} {{endblock}} multi_select / __init__.py From otree - snippets from otree.api import * doc = """ Question that lets you select multiple options (multi-select, multiple choice / multiple answer) """ class C(BaseConstants): NAME_IN_URL = 'select_multiple' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 LANGUAGES = ['english', 'german', 'french', 'spanish', 'italian', 'chinese'] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): english = models.BooleanField(blank=True) german = models.BooleanField(blank=True) french = models.BooleanField(blank=True) spanish = models.BooleanField(blank=True) italian = models.BooleanField(blank=True) chinese = models.BooleanField(blank=True) # PAGES class MyPage(Page): form_model = 'player' form_fields = C.LANGUAGES page_sequence = [MyPage] multi_select / MyPage.html From otree - snippets {{block content}} < p > What languages do you speak? Select all that apply. < / p > {{ for field in C.LANGUAGES}} < label > < input type = "checkbox" name = "{{ field }}" value = "1" > {{field}} < / label > < br > {{endfor}} < p > {{next_button}} < / p > {{endblock}} complex_form_layout / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'complex_form_layout' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): a1 = models.IntegerField() a2 = models.IntegerField() a3 = models.IntegerField() a4 = models.IntegerField() b1 = models.StringField() b2 = models.StringField() b3 = models.StringField() # PAGES class MyPage(Page): form_model = 'player' form_fields = ['a1', 'a2', 'a3', 'a4', 'b1', 'b2', 'b3'] @staticmethod def vars_for_template(player: Player): import random a_fields = ['a1', 'a2', 'a3', 'a4'] b_fields = ['b1', 'b2', 'b3'] random.shuffle(a_fields) random.shuffle(b_fields) return dict(a_fields=a_fields, b_fields=b_fields) page_sequence = [MyPage] complex_form_layout / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > < i > fields are in 2 sections, and randomized within a section. < / i > < / p > < p > Please answer the following questions about topic A < / p > {{ for field in a_fields}} {{formfield field}} {{endfor}} < p > Please answer the following questions about topic B < / p > {{ for field in b_fields}} {{formfield field}} {{endfor}} {{next_button}} {{endblock}} rank_widget / __init__.py From otree - snippets from otree.api import * doc = """ "Widget to rank/reorder items". See http://sortablejs.github.io/Sortable/ for more examples. """ class C(BaseConstants): NAME_IN_URL = 'rank_widget' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 CHOICES = ['Martini', 'Margarita', 'White Russian', 'Pina Colada', 'Gin & Tonic'] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): ranking = models.StringField() class MyPage(Page): form_model = 'player' form_fields = ['ranking'] class Results(Page): pass page_sequence = [MyPage, Results] rank_widget / Results.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > Your ranking is: {{player.ranking}} < / p > {{next_button}} {{endblock}} rank_widget / MyPage.html From otree - snippets {{block title}} Rank your favorite drinks {{endblock}} {{block content}} < ul id = "items" class ="list-group list-group-numbered" style="cursor: move" > {{ for choice in C.CHOICES}} < li data - id = "{{ choice }}" class ="list-group-item" > {{choice}} < / li > {{endfor}} < / ul > < script src = "https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js" > < / script > < script > let el = document.getElementById('items'); let sortable = Sortable.create(el, { onChange: function(evt) { document.getElementById('ranking').value = sortable.toArray().join(','); } }); < / script > < input type = "hidden" name = "ranking" id = "ranking" > {{formfield_errors 'ranking'}} < p > {{next_button}} < / p > {{endblock}} question_with_other_option / __init__.py From otree - snippets from otree.api import * doc = """ Menu with an 'other' option that lets you type in a valueInput manually """ class C(BaseConstants): NAME_IN_URL = 'question_with_other_option' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): native_language = models.StringField( choices=['German', 'English', 'Chinese', 'Turkish', 'Other'] ) native_language_other = models.StringField( label="You selected 'other'. What is your native language?" ) # PAGES class MyPage(Page): form_model = 'player' form_fields = ['native_language'] class MyPage2(Page): @staticmethod def is_displayed(player: Player): return player.native_language == 'Other' form_model = 'player' form_fields = ['native_language_other'] page_sequence = [MyPage, MyPage2] question_with_other_option / MyPage2.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} question_with_other_option / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} wait_page_timeout / Task.html From otree - snippets {{block title}} Task {{endblock}} {{block content}} < p > Continue the experiment... < / p > {{next_button}} {{endblock}} wait_page_timeout / __init__.py From otree - snippets from otree.api import * doc = """Timeout on a WaitPage (exit the experiment)""" class C(BaseConstants): NAME_IN_URL = 'wait_page_timeout' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 TIMEOUT = 15 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): import random for p in subsession.get_players(): p.completion_code = random.randint(10 ** 6, 10 ** 7) class Group(BaseGroup): pass class Player(BasePlayer): timeout = models.FloatField() completion_code = models.IntegerField() # PAGES class MyPage(Page): @staticmethod def before_next_page(player: Player, timeout_happened): import time # 15 seconds on wait page max player.timeout = time.time() + C.TIMEOUT class ResultsWaitPage(WaitPage): template_name = 'wait_page_timeout/ResultsWaitPage.html' @staticmethod def js_vars(player: Player): return dict(timeout=C.TIMEOUT) @staticmethod def vars_for_template(player: Player): import time timeout_happened = time.time() > player.timeout return dict(timeout_happened=timeout_happened) class Task(Page): pass page_sequence = [MyPage, ResultsWaitPage, Task] wait_page_timeout / MyPage.html From otree - snippets {{block title}} Welcome {{endblock}} {{block content}} < p > Press next to continue ... < / p > {{next_button}} {{endblock}} wait_page_timeout / ResultsWaitPage.html From otree - snippets {{extends 'otree/WaitPage.html'}} {{block title}} Please wait {{endblock}} {{block content}} {{ if timeout_happened}} < p > No other players showed up in time. Please submit this HIT with completion code < b > {{player.completion_code}} < / b > < / p > {{ else}} < p > If you are left waiting for longer than {{C.TIMEOUT}} seconds, the game will end. < / p > < script > setInterval(function() { window.location.reload(); }, js_vars.timeout * 1000); < / script > {{endif}} {{endblock}} detect_mobile / Task.html From otree - snippets {{block title}} Task. {{endblock}} {{block content}} < p > You are not using a mobile browser, so you can continue.< / p > {{next_button}} {{endblock}} detect_mobile / __init__.py From otree - snippets from otree.api import * doc = """Detect and block mobile browsers""" class C(BaseConstants): NAME_IN_URL = 'detect_mobile' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): is_mobile = models.BooleanField() # PAGES class MobileCheck(Page): form_model = 'player' form_fields = ['is_mobile'] def error_message(player: Player, values): if values['is_mobile']: return "Sorry, this experiment does not allow mobile browsers." class Task(Page): pass page_sequence = [MobileCheck, Task] detect_mobile / MobileCheck.html From otree - snippets {{block title}} Start {{endblock}} {{block content}} < input type = "hidden" name = "is_mobile" id = "is_mobile" > < p > Please click next. < / p > {{next_button}} < script > function isMobile() { const toMatch = [ / Android / i, / iPhone / i, / iPad / i, ]; return toMatch.some((item) = > navigator.userAgent.match(item)); } // here is an alternative technique that checks screen resolution // function isMobile() { // return ((window.innerWidth <= 800) & & (window.innerHeight <= 600)); //} document.getElementById('is_mobile').value = isMobile() ? 1: 0; < / script > {{endblock}} gbat_fallback_solo_task_part1 / __init__.py From otree - snippets from otree.api import * doc = """group_by_arrival_time timeout (continue with solo task)""" class C(BaseConstants): NAME_IN_URL = 'gbat_fallback_solo_task_part1' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def group_by_arrival_time_method(subsession, waiting_players): print('waiting_players', waiting_players) if len(waiting_players) >= 2: return waiting_players[:2] for player in waiting_players: if waiting_too_long(player): # make a single-player group. print('waiting too long, making 1 player group') return [player] class Group(BaseGroup): pass class Player(BasePlayer): favorite_color = models.StringField() def waiting_too_long(player: Player): participant = player.participant import time # assumes you set wait_page_arrival in PARTICIPANT_FIELDS. return time.time() - participant.wait_page_arrival > 60 class GBAT(WaitPage): group_by_arrival_time = True @staticmethod def app_after_this_page(player: Player, upcoming_apps): # if it's a solo group (1 player), skip this app # and go to the next app (which in this case is a # single-player task) if len(player.get_others_in_group()) == 0: return upcoming_apps[0] class GroupTask(Page): form_model = 'player' form_fields = ['favorite_color'] class MyWait(WaitPage): pass class Results(Page): pass page_sequence = [GBAT, GroupTask, MyWait, Results] gbat_fallback_solo_task_part1 / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > The colors chosen in your group were: < / p > < ul > {{ for p in group.get_players()}} < li > {{p.favorite_color}} < / li > {{endfor}} < / ul > {{next_button}} {{endblock}} gbat_fallback_solo_task_part1 / GroupTask.html From otree - snippets {{block content}} < p > Your game goes here... < / p > {{formfields}} {{next_button}} {{endblock}} gbat_fallback_smaller_group_part0 / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'gbat_fallback_smaller_group_part0' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class MyPage(Page): @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant import time participant.wait_page_arrival = time.time() page_sequence = [MyPage] gbat_fallback_smaller_group_part0 / MyPage.html From otree - snippets {{block title}} Welcome {{endblock}} {{block content}} < p > Welcome! Please press next. You will be placed in a group of 4. However, if not enough players show up, a smaller group may be formed with whoever is available. < / p > {{next_button}} {{endblock}} chat_from_scratch / __init__.py From otree - snippets from otree.api import * doc = """ Of course oTree has a readymade chat widget described here: https://otree.readthedocs.io/en/latest/multiplayer/chat.html But you can use this if you want a chat box that is more easily customizable, or if you want programmatic access to the chat messages. This app can also help you learn about live pages in general. """ class C(BaseConstants): NAME_IN_URL = 'chat_from_scratch' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class Message(ExtraModel): group = models.Link(Group) sender = models.Link(Player) text = models.StringField() def to_dict(msg: Message): return dict(sender=msg.sender.id_in_group, text=msg.text) # PAGES class MyPage(Page): @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def live_method(player: Player, data): my_id = player.id_in_group group = player.group if 'text' in data: text = data['text'] msg = Message.create(group=group, sender=player, text=text) return {0: [to_dict(msg)]} return {my_id: [to_dict(msg) for msg in Message.filter(group=group)]} page_sequence = [MyPage] chat_from_scratch / chat.html From otree - snippets < div id = "chat_messages" > < / div > < div > < input type = "text" id = "chat_input" > < button type = "button" onclick = "sendMsg()" > Send < / button > < / div > < script > let chat_input = document.getElementById('chat_input'); chat_input.addEventListener("keydown", function(event) { if (event.key === "Enter") { sendMsg(); } }); function sendMsg() { let text = chat_input.value.trim(); if (text) { liveSend({'text': text}); } chat_input.value = ''; } let chat_messages = document.getElementById('chat_messages'); function liveRecv(messages) { for (let msg of messages) { let msgSpan = document.createElement('span'); msgSpan.textContent = msg.text; let sender = msg.sender == = js_vars.my_id ? 'Me': `Player ${msg.sender} `; let row = ` < div > < b >${sender} < / b >: ${msgSpan.innerHTML} < / div > `; chat_messages.insertAdjacentHTML('beforeend', row); } } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > chat_from_scratch / MyPage.html From otree - snippets {{block content}} {{include_sibling 'chat.html'}} {{endblock}} are_you_sure / __init__.py From otree - snippets from otree.api import * doc = """ 'Are you sure?' popup based on the user's input """ class C(BaseConstants): NAME_IN_URL = 'are_you_sure' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): contribution = models.CurrencyField( min=0, max=100, label="How much of your 100 points do you want to contribute?" ) reason = models.LongStringField( blank=True, label="Please write a message to your teammates explaining your decision", ) # PAGES class MyPage(Page): form_model = 'player' form_fields = ['contribution', 'reason'] page_sequence = [MyPage] are_you_sure / MyPage.html From otree - snippets {{block content}} < p > < i > This page warns if the user contributes 0 or their explanation is too short. < / i > < / p > {{formfields}} < button type = "button" class ="btn btn-primary" onclick="checkSubmit()" > Next < / button > < script > function checkSubmit() { let form = document.getElementById('form'); let isValid = form.reportValidity(); if (!isValid) return; let warnings = []; let contribution = document.getElementsByName('contribution')[0].value; if (contribution === '0') { warnings.push("Are you sure you don't want to contribute anything?"); } let reason = document.getElementsByName('reason')[0].value; if (reason.length < 10) { warnings.push("Are you sure you don't want to give a longer explanation?") } if (warnings.length > 0) { warnings.push("Press OK to proceed anyway.") let confirmed = window.confirm(warnings.join('\r\n')); if (!confirmed) return; } form.submit(); } < / script > {{endblock}} longitudinal / Bridge.html From otree - snippets {{block content}} < p > Thank you for participating in part 1. < / p > < p > Please come back after < b > {{player.part2_start_time_readable}} < / b > to take part in the next phase. < / p > {{endblock}} longitudinal / Part1.html From otree-snippets {{block title}} Survey {{endblock}} {{block content}} < p > < i > The first phase of your experiment goes here...< / i > < / p > {{formfields}} {{next_button}} {{endblock}} longitudinal / __init__.py From otree-snippets from otree.api import * doc = """ Longitudinal study (2-part study taking place across days/weeks) Another way to do longitudinal studies is just to give participants a Room URL. Since that URL is persistent, you can create a new session when the next phase has begun. But the technique here has the advantage of storing both phases together in a single session. For example, you can easily compare the user's answer to their answer in the previous phase. """ class C(BaseConstants): NAME_IN_URL = 'longitudinal' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): question = models.StringField() part2_start_time = models.FloatField() part2_start_time_readable = models.StringField() # PAGES class Part1(Page): @staticmethod def before_next_page(player: Player, timeout_happened): from datetime import datetime, timedelta t = datetime.now() + timedelta(weeks=1) # or can make it for a specific date: # start = datetime.strptime('2022-07-15', '%Y-%m-%d') # .timestamp() gives you an integer (a.k.a. 'epoch time') player.part2_start_time = t.timestamp() # print('player.part2_start_time is', player.part2_start_time) # this gives you a formatted date you can display to users player.part2_start_time_readable = t.strftime('%A, %B %d') def still_waiting_for_part_2(player: Player): import time # returns True if the current time is before the designated start time return time.time() < player.part2_start_time class Bridge(Page): """ If the user arrives at this page after part 2 is ready, this page will be skipped entirely. """ @staticmethod def is_displayed(player: Player): return still_waiting_for_part_2(player) @staticmethod def before_next_page(player: Player, timeout_happened): return "Player somehow tried to proceed past a page with no next button" class Part2(Page): pass page_sequence = [Part1, Bridge, Part2] longitudinal / Part2.html From otree - snippets {{block title}} Survey {{endblock}} {{block content}} < p > < i > The second phase of your experiment goes here... < / i > < / p > {{formfields}} {{next_button}} {{endblock}} comprehension_test_simple / __init__.py From otree - snippets from otree.api import * doc = """ Simple version of comprehension test """ class C(BaseConstants): NAME_IN_URL = 'comprehension_test_simple' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): quiz1 = models.IntegerField(label='What is 2 + 2?') quiz2 = models.IntegerField(label="What year did COVID-19 start?") quiz3 = models.BooleanField(label="Is 9 a prime number?") class MyPage(Page): form_model = 'player' form_fields = ['quiz1', 'quiz2', 'quiz3'] @staticmethod def error_message(player: Player, values): solutions = dict(quiz1=4, quiz2=2019, quiz3=False) if values != solutions: return "One or more answers were incorrect." class Results(Page): pass page_sequence = [MyPage, Results] comprehension_test_simple / Results.html From otree - snippets {{block title}} Thank you {{endblock}} {{block content}} < p > You answered all questions correctly < / p > {{endblock}} comprehension_test_simple / MyPage.html From otree - snippets {{block title}} Comprehension test {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} random_num_rounds / End.html From otree - snippets {{block title}} End {{endblock}} {{block content}} The session is finished. {{endblock}} random_num_rounds / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'random_num_rounds' PLAYERS_PER_GROUP = None NUM_ROUNDS = 20 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): import random for p in subsession.get_players(): p.participant.num_rounds = random.randint(1, 20) class Group(BaseGroup): pass class Player(BasePlayer): num_rounds = models.IntegerField() # PAGES class MyPage(Page): @staticmethod def is_displayed(player: Player): """ Skip this page if the round number has exceeded the participant's designated number of rounds. """ participant = player.participant return player.round_number < participant.num_rounds class End(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS page_sequence = [MyPage, End] random_num_rounds / MyPage.html From otree - snippets {{block title}} Round {{subsession.round_number}} {{endblock}} {{block content}} < p > This player will continue for {{participant.num_rounds}} rounds. < / p > {{next_button}} {{endblock}} persist_raw / __init__.py From otree - snippets from otree.api import * doc = """ Sliders and checkboxes that don't get wiped out on form reload. Also works for text/number inputs, etc. """ class C(BaseConstants): NAME_IN_URL = 'persist_raw' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): f_int = models.IntegerField(min=10) f_bool1 = models.BooleanField(blank=True) f_bool2 = models.BooleanField(blank=True) f_bool3 = models.BooleanField(blank=True) f_bool4 = models.BooleanField(blank=True) f_bool5 = models.BooleanField(blank=True) # PAGES class MyPage(Page): form_model = 'player' form_fields = [ 'f_int', 'f_bool1', 'f_bool2', 'f_bool3', 'f_bool4', 'f_bool5', ] page_sequence = [MyPage] persist_raw / MyPage.html From otree - snippets {{block content}} < p > If you 've used raw HTML widgets (slider/checkbox), you may have noticed that they are wiped out on when the form is re - rendered to show errors. This page contains simple code to overcome that limitation. < / p > < p > To test, modify the form fields, then submit the page. (The form will fail validation until you set the slider to the correct value.) < / p > < label class ="col-form-label" > Here is a slider: < / label > < div style = "display: flex" > 0 & nbsp; < input type = "range" name = "f_int" min = "0" max = "10" style = "flex: 1" class ="persist" > & nbsp; 10 < / div > {{formfield_errors 'f_int'}} < br > < p > Here are some checkboxes: < / p > < input type = "checkbox" name = "f_bool1" value = "1" class ="persist" > < input type = "checkbox" name = "f_bool2" value = "1" class ="persist" > < input type = "checkbox" name = "f_bool3" value = "1" class ="persist" > < input type = "checkbox" name = "f_bool4" value = "1" class ="persist" > < input type = "checkbox" name = "f_bool5" value = "1" class ="persist" > < br > < br > {{next_button}} { # INSTRUCTIONS (1) make sure your _static / folder contains persist - raw.js (2) copy the below 'script' tag into your template (2) add class ="persist" to your raw HTML inputs # } < script src = "{{ static 'persist-raw.js' }}" > < / script > {{endblock}} dropout_end_game / DropoutHappened.html From otree - snippets {{block content}} < p > A player in your group dropped out. Therefore, you will be forwarded to the next app. < / p > {{next_button}} {{endblock}} dropout_end_game / DropoutTest.html From otree - snippets {{block title}} Dropout check {{endblock}} {{block content}} < p > Important: click "next" before the timeout occurs. Otherwise you will be considered a dropout. < / p > {{next_button}} {{endblock}} dropout_end_game / __init__.py From otree - snippets from otree.api import * doc = """ Dropout detection for multiplayer game (end the game) """ class C(BaseConstants): NAME_IN_URL = 'dropout_end_game' PLAYERS_PER_GROUP = None NUM_ROUNDS = 5 class Subsession(BaseSubsession): pass class Group(BaseGroup): has_dropout = models.BooleanField(initial=False) class Player(BasePlayer): is_dropout = models.BooleanField() class Game(Page): timeout_seconds = 10 class DropoutTest(Page): timeout_seconds = 10 @staticmethod def before_next_page(player: Player, timeout_happened): group = player.group if timeout_happened: group.has_dropout = True player.is_dropout = True class WaitForOthers(WaitPage): pass class DropoutHappened(Page): @staticmethod def is_displayed(player: Player): group = player.group return group.has_dropout @staticmethod def app_after_this_page(player: Player, upcoming_apps): return upcoming_apps[0] page_sequence = [Game, DropoutTest, WaitForOthers, DropoutHappened] dropout_end_game / Game.html From otree - snippets {{block title}} Game, round {{subsession.round_number}} {{endblock}} {{block content}} < p > < i > Your game goes here... < / i > < / p > {{formfields}} {{next_button}} {{endblock}} configurable_players_per_group / __init__.py From otree - snippets from otree.api import * doc = """ Configurable players per group. See here: https://otree.readthedocs.io/en/latest/treatments.html#configure-sessions """ class C(BaseConstants): NAME_IN_URL = 'configurable_players_per_group' # Since Constants does not have access to the session config, # (it is loaded when the server starts, rather than for each session) # we set the groups manually inside creating_session. PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): session = subsession.session ppg = session.config['players_per_group'] players = subsession.get_players() matrix = [] for i in range(0, len(players), ppg): matrix.append(players[i: i + ppg]) # print('matrix is', matrix) subsession.set_group_matrix(matrix) class Group(BaseGroup): pass class Player(BasePlayer): pass class MyPage(Page): pass page_sequence = [MyPage] configurable_players_per_group / MyPage.html From otree - snippets image_choices / __init__.py From otree - snippets from otree.api import * doc = """ Images in radio button choices """ def make_image_data(image_names): return [dict(name=name, path='shapes/{}'.format(name)) for name in image_names] class C(BaseConstants): NAME_IN_URL = 'image_choices' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): img_choice = models.StringField() # PAGES class MyPage(Page): form_model = 'player' form_fields = ['img_choice'] @staticmethod def vars_for_template(player: Player): image_names = [ 'circle-blue.svg', 'plus-green.svg', 'star-red.svg', 'triangle-yellow.svg', ] return dict(image_data=make_image_data(image_names)) page_sequence = [MyPage] image_choices / MyPage.html From otree - snippets {{block content}} < p > Choose your favorite image. < / p > {{ for image in image_data}} < label style = "text-align: center" > < img src = "{{ static image.path }}" width = "200px" > < br > < input type = "radio" name = "img_choice" value = "{{ image.name }}" class ="persist" > < / label > {{endfor}} {{formfield_errors 'img_choice'}} < br > {{next_button}} < script src = "{{ static 'persist-raw.js' }}" > < / script > {{endblock}} pay_random_round / __init__.py From otree - snippets from otree.api import * doc = """ Select a random round for payment """ class C(BaseConstants): NAME_IN_URL = 'pay_random_round' PLAYERS_PER_GROUP = None NUM_ROUNDS = 4 ENDOWMENT = cu(100) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): give_amount = models.CurrencyField( min=0, max=100, label="How much do you want to give?" ) # PAGES class MyPage(Page): form_model = 'player' form_fields = ['give_amount'] @staticmethod def before_next_page(player: Player, timeout_happened): import random participant = player.participant # if it's the last round if player.round_number == C.NUM_ROUNDS: random_round = random.randint(1, C.NUM_ROUNDS) participant.selected_round = random_round player_in_selected_round = player.in_round(random_round) player.payoff = C.ENDOWMENT - player_in_selected_round.give_amount class Results(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS page_sequence = [MyPage, Results] pay_random_round / Results.html From otree - snippets {{block content}} {{ if subsession.round_number == C.NUM_ROUNDS}} < p > Round {{participant.selected_round}} was randomly selected for payment. Your final payoff is therefore {{player.payoff}}. < / p > {{endif}} {{endblock}} pay_random_round / MyPage.html From otree - snippets {{block title}} Round {{subsession.round_number}} {{endblock}} {{block content}} < p > You have {{C.ENDOWMENT}} to split between you and another player. < / p > {{formfields}} {{next_button}} {{endblock}} quiz_with_explanation / __init__.py From otree - snippets from otree.api import * doc = """ Quiz with explanation. Re-display the previous page's form as read-only, with answers/explanation. """ class C(BaseConstants): NAME_IN_URL = 'quiz_with_explanation' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 def get_quiz_data(): return [ dict( name='a', solution=True, explanation="3 is prime. It has no factorization other than 1 and itself.", ), dict( name='b', solution=False, explanation="2 + 2 is 4.", ), ] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): a = models.BooleanField(label="Is 3 a prime number?") b = models.IntegerField(label="What is 2 + 2?") class MyPage(Page): form_model = 'player' form_fields = ['a', 'b'] @staticmethod def vars_for_template(player: Player): fields = get_quiz_data() return dict(fields=fields, show_solutions=False) class Results(Page): form_model = 'player' form_fields = ['a', 'b'] @staticmethod def vars_for_template(player: Player): fields = get_quiz_data() # we add an extra entry 'is_correct' (True/False) to each field for d in fields: d['is_correct'] = getattr(player, d['name']) == d['solution'] return dict(fields=fields, show_solutions=True) @staticmethod def error_message(player: Player, values): for field in values: if getattr(player, field) != values[field]: return "A field was somehow changed but this page is read-only." page_sequence = [MyPage, Results] quiz_with_explanation / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < style > / * interestingly, 'readonly' doesn 't apply to radio buttons. so we set 'pointer-events: none' to prevent clicking a radio. (it can also be done through JS but doesn't hurt to have this extra measure) (note: radio buttons can also be changed using the keyboard) note: we don 't set ' disabled ' because disabled inputs don' t get submitted by the form, and therefore the server would complain that the form is missing. * / input, label { pointer - events: none; } .solution - incorrect { color: red; } .solution - correct { color: green; } < / style > < p > Here are your answers along with the solutions.< / p > {{include_sibling 'form.html'}} {{next_button}} < script > // for (let input of document.getElementsByTagName('input')) { input.readOnly = true; } // workaround for radio buttons.disable all radio buttons that aren't already checked. // this prevents changing a radio. $(':radio:not(:checked)').attr('disabled', true); < / script > {{endblock}} quiz_with_explanation / MyPage.html From otree-snippets {{block title}} Quiz {{endblock}} {{block content}} {{include_sibling 'form.html'}} {{next_button}} {{endblock}} quiz_with_explanation / form.html From otree-snippets {{for d in fields}} {{formfield d.name}} {{if show_solutions}} {{if d.is_correct}} < p class ="solution-correct" > Correct. < / p > {{ else}} < p class ="solution-incorrect" > {{d.explanation}} < / p > {{endif}} {{endif}} {{endfor}} gbat_keep_same_groups_part0 / __init__.py From otree - snippets from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'gbat_keep_same_groups_part0' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): pass page_sequence = [MyPage] gbat_keep_same_groups_part0 / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > < i > This would just be the first app that appears first in the app sequence, to filter out some users before doing group_by_arrival_time. Maybe it asks for their consent, or has them do some real-effort task, to filter out people who are likely to drop out. < / i > < / p > {{next_button}} {{endblock}} sequential / __init__.py From otree - snippets from otree.api import * doc = """ Sequential game (asymmetric) """ class C(BaseConstants): NAME_IN_URL = 'sequential' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 MAIN_TEMPLATE = __name__ + '/Decide.html' class Subsession(BaseSubsession): pass class Group(BaseGroup): mixer = models.StringField( choices=['Pineapple juice', 'Orange juice', 'Cola', 'Milk'], label="Choose a mixer", widget=widgets.RadioSelect, ) liqueur = models.StringField( choices=['Blue curacao', 'Triple sec', 'Amaretto', 'Kahlua'], label="Choose a liqueur", widget=widgets.RadioSelect, ) spirit = models.StringField( choices=['Vodka', 'Rum', 'Gin', 'Tequila'], label="Choose a spirit", widget=widgets.RadioSelect, ) class Player(BasePlayer): pass # PAGES class P1(Page): form_model = 'group' form_fields = ['mixer'] template_name = C.MAIN_TEMPLATE @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 class WaitPage1(WaitPage): pass class P2(Page): form_model = 'group' form_fields = ['liqueur'] template_name = C.MAIN_TEMPLATE @staticmethod def is_displayed(player: Player): return player.id_in_group == 2 class WaitPage2(WaitPage): pass class P3(Page): form_model = 'group' form_fields = ['spirit'] template_name = C.MAIN_TEMPLATE @staticmethod def is_displayed(player: Player): return player.id_in_group == 3 class WaitPage3(WaitPage): pass class Results(Page): @staticmethod def vars_for_template(player: Player): group = player.group return dict(players_with_contributions=group.get_players()) page_sequence = [P1, WaitPage1, P2, WaitPage2, P3, WaitPage3, Results] sequential / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > The cocktail consists of {{group.mixer}}, {{group.liqueur}}, and {{group.spirit}} < / p > {{endblock}} sequential / Decide.html From otree - snippets {{block title}} Make a cocktail! {{endblock}} {{block content}} < ul > < li > This is a sequential game with {{C.PLAYERS_PER_GROUP}} players.< / li > < li > You are player {{player.id_in_group}}. < / li > < li > The objective is to make a cocktail with 3 ingredients.Each player chooses one ingredient.< / li > < / ul > {{ if player.id_in_group >= 2}} < p > Player 1 chose {{group.mixer}}. < / p > {{endif}} {{ if player.id_in_group >= 3}} < p > Player 2 chose {{group.liqueur}}. < / p > {{endif}} {{formfields}} {{next_button}} {{endblock}} comprehension_test_complex / Failed.html From otree - snippets {{block content}} Sorry, you gave too many wrong answers to the comprehension test. {{endblock}} comprehension_test_complex / __init__.py From otree - snippets from otree.api import * doc = """ Comprehension test. If the user fails too many times, they exit. """ class C(BaseConstants): NAME_IN_URL = 'comprehension_test_complex' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): num_failed_attempts = models.IntegerField(initial=0) failed_too_many = models.BooleanField(initial=False) quiz1 = models.IntegerField(label='What is 2 + 2?') quiz2 = models.StringField( label='What is the capital of Canada?', choices=['Ottawa', 'Toronto', 'Vancouver'], ) quiz3 = models.IntegerField(label="What year did COVID-19 start?") quiz4 = models.BooleanField(label="Is 9 a prime number") class MyPage(Page): form_model = 'player' form_fields = ['quiz1', 'quiz2', 'quiz3', 'quiz4'] @staticmethod def error_message(player: Player, values): # alternatively, you could make quiz1_error_message, quiz2_error_message, etc. # but if you have many similar fields, this is more efficient. solutions = dict(quiz1=4, quiz2='Ottawa', quiz3=2019, quiz4=False) # error_message can return a dict whose keys are field names and whose # values are error messages errors = {name: 'Wrong' for name in solutions if values[name] != solutions[name]} # print('errors is', errors) if errors: player.num_failed_attempts += 1 if player.num_failed_attempts >= 3: player.failed_too_many = True # we don't return any error here; just let the user proceed to the # next page, but the next page is the 'failed' page that boots them # from the experiment. else: return errors class Failed(Page): @staticmethod def is_displayed(player: Player): return player.failed_too_many class Results(Page): pass page_sequence = [MyPage, Failed, Results] comprehension_test_complex / Results.html From otree - snippets {{block title}} Thank you {{endblock}} {{block content}} < p > You answered all questions correctly < / p > {{endblock}} comprehension_test_complex / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} pass_data_between_apps_part2 / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'pass_data_between_apps2' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): pass page_sequence = [MyPage] pass_data_between_apps_part2 / MyPage.html From otree - snippets {{block title}} App 2 {{endblock}} {{block content}} < p > In the previous app, you said your main language is < b > {{participant.language}} < / b >. < / p > {{endblock}} rank_players / __init__.py From otree - snippets from otree.api import * doc = """ Rank players """ class C(BaseConstants): NAME_IN_URL = 'rank_players' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): age = models.IntegerField(label="Enter your age") rank = models.IntegerField() # PAGES class MyPage(Page): form_model = 'player' form_fields = ['age'] class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): players = group.get_players() # to do descending, use -p.age players.sort(key=lambda p: p.age) for i in range(len(players)): # this code checks if there is a tie and then assigns the same rank # if you don't need to deal with ties, then you can delete this. if i > 0 and players[i].age == players[i - 1].age: rank = players[i - 1].rank else: rank = i + 1 players[i].rank = rank class Results(Page): @staticmethod def vars_for_template(player: Player): group = player.group return dict(num_players=len(group.get_players())) page_sequence = [MyPage, ResultsWaitPage, Results] rank_players / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > Your age is {{player.age}}. In your group of {{num_players}} players, your rank is {{player.rank}} (youngest to oldest). < / p > {{endblock}} rank_players / MyPage.html From otree - snippets {{block content}} {{formfields}} {{next_button}} {{endblock}} placeholder / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'placeholder' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): pass page_sequence = [MyPage] placeholder / MyPage.html From otree - snippets {{block title}} Placeholder {{endblock}} {{block content}} < p > < i > This app is just a placeholder. < / i > < / p > {{endblock}} groups_csv / __init__.py From otree - snippets from otree.api import * doc = """ Reads groups from a CSV file. Inside this app, you will find a groups6.csv, which defines the groups in the case where there are 6 players. You can edit the file in Excel, or in plain text. In the below example, there are 5 rows, defining 5 rounds. In each row, empty cells are used to separate groups. So, in round 1, there are 3 groups: players 1&4, 2&5, 3&6: 1,4,,2,5,,3,6 1,2,,3,4,,6,5 1,3,,6,2,,5,4 1,6,,5,3,,4,2 1,5,,4,6,,2,3 If you want to create a session with a different number of players, such as 12, you would need to create a file called groups12.csv. """ def make_group(comma_delim_string): return [int(x) for x in comma_delim_string.split(',')] class C(BaseConstants): NAME_IN_URL = 'groups_csv' PLAYERS_PER_GROUP = None NUM_ROUNDS = 5 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # FUNCTIONS def creating_session(subsession: Subsession): session = subsession.session if subsession.round_number == 1: num_participants = session.num_participants fn = 'groups_csv/groups{}.csv'.format(num_participants) with open(fn) as f: matrices = [] for line in f: line = line.strip() group_specs = line.split(',,') matrix = [make_group(spec) for spec in group_specs] matrices.append(matrix) session.matrices = matrices this_round_matrix = session.matrices[subsession.round_number - 1] subsession.set_group_matrix(this_round_matrix) # print('this_round_matrix', this_round_matrix) # PAGES class MyPage(Page): pass page_sequence = [ MyPage, ] groups_csv / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > No content here; this app is just to demonstrate group shuffling (look at the groups in the admin interface). < / p > {{next_button}} {{endblock}} radio_switching_point / __init__.py From otree - snippets from otree.api import * doc = """ Table where each row has a left/right choice, like the strategy method. This app enforces a single switching point """ class C(BaseConstants): NAME_IN_URL = 'radio_switching_point' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): left_side_amount = models.IntegerField(initial=10) switching_point = models.IntegerField() # PAGES class Decide(Page): form_model = 'player' form_fields = ['switching_point'] @staticmethod def vars_for_template(player: Player): return dict(right_side_amounts=range(10, 21, 1)) class Results(Page): pass page_sequence = [Decide, Results] radio_switching_point / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > Your switching point was {{player.switching_point}} < / p > {{endblock}} radio_switching_point / Decide.html From otree - snippets {{block title}} Choose a value for each row {{endblock}} {{block content}} < input type = "hidden" name = "switching_point" id = "id_switching_point" > {{formfield_errors 'switching_point'}} < table class ="table table-striped" > < colgroup > < col width = "45%" > < col width = "10%" > < col width = "45%" > < / colgroup > < tr > < td align = "right" > < b > Option A < / b > < / td > < td > < / td > < td align = "left" > < b > Option B < / b > < / td > < / tr > {{ for amount in right_side_amounts}} < tr > < td align = "right" > < b > {{player.left_side_amount}} < / b > now < td align = "middle" > < input type = "radio" value = "left" name = "{{ amount }}" required > & nbsp; & nbsp; < input type = "radio" name = "{{ amount }}" value = "right" data - amount = "{{ amount }}" required > < / td > < td align = "left" > < b > {{amount}} < / b > next month < / tr > {{endfor}} < / table > < button type = "button" class ="btn btn-primary" onclick="submitForm()" > Next < / button > {{endblock}} {{block scripts}} < script > let allRadios = document.querySelectorAll('input[type=radio]') function submitForm() { let form = document.getElementById('form'); if (form.reportValidity()) { let switchingPoint = document.getElementById('id_switching_point'); let allChoicesAreOnLeft = true; for (let radio of allRadios) { if (radio.value === 'right' & & radio.checked) { switchingPoint.value = radio.dataset.amount; allChoicesAreOnLeft = false; break; } } if (allChoicesAreOnLeft) { // '9999' represents the valueInput if the user didn't click the right side for any choice // it means their switching point is off the scale.you can change 9999 to some other valueInput // that is larger than any right-hand-side choice. switchingPoint.value = '9999'; } form.submit(); } } function onRadioClick(evt) { let clickedRadio = evt.target; let afterClickedRadio = false; let clickedRightRadio = clickedRadio.value == = 'right'; for (let aRadio of allRadios) { if (aRadio == = clickedRadio) { afterClickedRadio = true; continue; } if (clickedRightRadio & & afterClickedRadio & & aRadio.value === 'right') { aRadio.checked = true; } if (!clickedRightRadio & & !afterClickedRadio & & aRadio.value == = 'left') { aRadio.checked = true; } } } document.addEventListener("DOMContentLoaded", function(event) { for (let radio of document.querySelectorAll('input[type=radio]')) { radio.onchange = onRadioClick; } }); < / script > {{endblock}} rank_topN / __init__.py From otree - snippets from otree.api import * doc = """ Ranking your top N choices from a list of options. """ class C(BaseConstants): NAME_IN_URL = 'rank_topN' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 CHOICES = ['Martini', 'Margarita', 'White Russian', 'Pina Colada', 'Gin & Tonic'] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass def make_rank_field(label): return models.StringField(choices=C.CHOICES, label=label) class Player(BasePlayer): rank1 = make_rank_field("Top choice") rank2 = make_rank_field("Second choice") rank3 = make_rank_field("Third choice") class MyPage(Page): form_model = 'player' form_fields = ['rank1', 'rank2', 'rank3'] @staticmethod def error_message(player: Player, values): choices = [values['rank1'], values['rank2'], values['rank3']] # set() gives you distinct elements. if a list's length is different from its # set length, that means it must have duplicates. if len(set(choices)) != len(choices): return "You cannot choose the same item twice" class Results(Page): pass page_sequence = [MyPage, Results] rank_topN / Results.html From otree - snippets {{block content}} < p > Your top choices are {{player.rank1}}, {{player.rank2}}, and {{player.rank3}}. < / p > {{endblock}} rank_topN / MyPage.html From otree - snippets {{block title}} Rank your favorite drinks {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} gbat_treatments_complex / __init__.py From otree - snippets from otree.api import * doc = """ Similar to the basic gbat_treatments app, except: - Treatments are balanced rather than independently randomized. - The game persists for multiple rounds """ class C(BaseConstants): NAME_IN_URL = 'gbat_treatments_complex' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 3 # boolean works when there are 2 TREATMENTS # if you have >2 TREATMENTS, change this to numbers or strings like # [1, 2, 3] or ['A', 'B', 'C'], etc. TREATMENTS = [True, False] class Subsession(BaseSubsession): num_groups_created = models.IntegerField(initial=0) class Group(BaseGroup): pass class Player(BasePlayer): pass class GBATWaitPage(WaitPage): group_by_arrival_time = True @staticmethod def is_displayed(player: Player): """only do GBAT in the first round. this way, players stay in the same group for all rounds.""" return player.round_number == 1 @staticmethod def after_all_players_arrive(group: Group): subsession = group.subsession # % is the modulus operator. # so when num_groups_created exceeds the max list index, # we go back to 0, thus creating a cycle. idx = subsession.num_groups_created % len(C.TREATMENTS) treatment = C.TREATMENTS[idx] for p in group.get_players(): # since we want the treatment to persist for all rounds, we need to assign it # in a participant field (which persists across rounds) # rather than a group field, which is specific to the round. p.participant.time_pressure = treatment subsession.num_groups_created += 1 class MyPage(Page): pass page_sequence = [GBATWaitPage, MyPage] gbat_treatments_complex / MyPage.html From otree - snippets {{block title}} Round {{subsession.round_number}} {{endblock}} {{block content}} < p > Your group is assigned to the {{ if participant.time_pressure}} "time-pressure" {{ else}} "non-time-pressure" {{endif}} treatment. < / p > {{next_button}} {{endblock}} random_num_rounds_multiplayer_end / __init__.py From otree - snippets from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'random_num_rounds_multiplayer_end' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # PAGES class MyPage(Page): pass page_sequence = [MyPage] random_num_rounds_multiplayer_end / Results.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} {{next_button}} {{endblock}} random_num_rounds_multiplayer_end / MyPage.html From otree - snippets {{block content}} Thank you. {{endblock}} slider_graphic / __init__.py From otree - snippets from otree.api import * doc = """ An image that changes when you move a slider. If your image is a some kind of chart, it's better to use Highcharts than static images. See the SVO example. """ class C(BaseConstants): NAME_IN_URL = 'slider_graphic' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): feeling = models.IntegerField(min=0, max=3) # PAGES class MyPage(Page): form_model = 'player' form_fields = ['feeling'] @staticmethod def vars_for_template(player: Player): img_paths = ['slider_graphic/{}.svg'.format(i) for i in range(4)] return dict(img_paths=img_paths) page_sequence = [MyPage] slider_graphic / MyPage.html From otree - snippets {{block content}} < style > .slider - graphic { display: none; width: 6 em; } < / style > < p > Drag the slider to indicate how you feel right now. < / p > < input type = "range" name = "feeling" value = "1" min = "0" max = "3" oninput = "changeGraphic(this)" > {{ for img_path in img_paths}} < img src = "{{ static img_path }}" class ="slider-graphic" > {{endfor}} < script > let graphics = document.getElementsByClassName('slider-graphic'); function changeGraphic(input) { for (let img of graphics) { img.style.display = 'none'; } graphics[parseInt(input.value)].style.display = 'block'; } < / script > {{next_button}} {{endblock}} input_calculation / __init__.py From otree - snippets from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'input_calculation' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 APR = 0.07 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): amount = models.CurrencyField(min=0, max=100000) num_years = models.IntegerField(min=1, max=50) # PAGES class MyPage(Page): form_model = 'player' form_fields = ['amount', 'num_years'] @staticmethod def js_vars(player: Player): return dict(APR=C.APR) page_sequence = [MyPage] input_calculation / Results.html From otree - snippets {{block content}} < p > Thank you... < / p > {{endblock}} input_calculation / MyPage.html From otree - snippets {{block content}} < p > Choose what investment to make at an APR of {{C.APR}} < / p > {{formfields}} < br > < p > Your investment will be worth: < / p > < h2 > < span id = "projection" > < / span > < small > points < / small > < / h2 > {{next_button}} < script > let amountInput = document.getElementsByName('amount')[0]; let numYearsInput = document.getElementsByName('num_years')[0]; let projectionEle = document.getElementById('projection'); function recalc() { let amount = parseFloat(amountInput.value); let numYears = parseInt(numYearsInput.value); // isNaN is the javascript function that checks whether the value is a valid // number.need to check this because the field might be empty or // the user might have typed something other than a number. if (isNaN(amount) | | isNaN(numYears)) { projectionEle.innerText = ''; } else { let projection = amount * Math.pow((1 + js_vars.APR), numYears); projectionEle.innerText = Math.round(projection); } } amountInput.oninput = recalc; numYearsInput.oninput = recalc; < / script > {{endblock}} radio / __init__.py From otree - snippets from otree.api import * doc = """ Radio buttons in various layouts, looping over radio choices """ class C(BaseConstants): NAME_IN_URL = 'radio' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): f1 = models.IntegerField( widget=widgets.RadioSelect, choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ) f2 = models.IntegerField( widget=widgets.RadioSelect, choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ) # PAGES class MyPage(Page): form_model = 'player' form_fields = ['f1', 'f2'] page_sequence = [MyPage] radio / MyPage.html From otree - snippets {{block content}} < p > < i > Radio buttons without labels(visual / analog scale, similar to a slider) < / i > < / p > < p > Least & nbsp; {{ for choice in form.f1}} {{choice}} {{endfor}} & nbsp; Most < / p > {{formfield_errors 'f1'}} < br > < p > < i > Labels under radio buttons < / i > < / p > < div style = "display: flex" > {{ for choice in form.f2}} < div style = "flex: 1; text-align: center" > {{choice}} < br > < span style = "text-align: center" > {{choice.label}} < / span > < / div > {{endfor}} < / div > {{formfield_errors 'f2'}} < br > { # todo: radio buttons laid out individually, no loop (by index) #} {{next_button}} {{endblock}} appcopy1 / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'appcopy1' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): bbb = models.IntegerField(widget=widgets.RadioSelectHorizontal) def bbb_choices(player: Player): return [1, 2, 3] class MyPage(Page): # every page needs an explicit template_name template_name = 'appcopy1/MyPage.html' form_model = 'player' form_fields = ['bbb'] page_sequence = [MyPage] appcopy1 / MyPage.html From otree - snippets {{block title}} App A {{endblock}} {{block content}} < p > < i > This app gets repeated, with another app in between.< / i > < / p > {{formfields}} {{next_button}} {{endblock}} pay_random_app3 / PayRandomApp.html From otree - snippets {{block content}} < p > A random app will now be chosen for payment.< / p > {{next_button}} {{endblock}} pay_random_app3 / __init__.py From otree - snippets from otree.api import * doc = """ App where we choose the app to be paid """ class C(BaseConstants): NAME_IN_URL = 'pay_random_app3' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): app_to_pay = models.StringField() class PayRandomApp(Page): @staticmethod def before_next_page(player: Player, timeout_happened): import random participant = player.participant # print('participant.app_payoffs is', participant.app_payoffs) apps = [ 'pay_random_app1', 'pay_random_app2', ] app_to_pay = random.choice(apps) participant.payoff = participant.app_payoffs[app_to_pay] player.app_to_pay = app_to_pay class Results(Page): pass page_sequence = [PayRandomApp, Results] pay_random_app3 / Results.html From otree - snippets {{block title}} Final Results {{endblock}} {{block content}} < p > The app that was randomly chosen for payment is {{player.app_to_pay}}. You payoff from that app (and therefore your total payoff) is {{participant.payoff}}. < / p > {{endblock}} min_time_on_page / Page1.html From otree - snippets {{block title}} Page 1 {{endblock}} {{block content}} < p > < i > Click next... < / i > < / p > {{next_button}} {{endblock}} min_time_on_page / Page2.html From otree - snippets {{block title}} Page 2 {{endblock}} {{block content}} < p > You must stay on this page for at least 10 seconds.< / p > {{next_button}} {{endblock}} min_time_on_page / Page3.html From otree - snippets {{block title}} Page 3 {{endblock}} {{block content}} {{endblock}} min_time_on_page / __init__.py From otree - snippets from otree.api import * doc = """ Minimum time on a page """ class C(BaseConstants): NAME_IN_URL = 'min_time_on_page' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): page_pass_time = models.FloatField() class Page1(Page): @staticmethod def before_next_page(player: Player, timeout_happened): import time player.page_pass_time = time.time() + 10 # PAGES class Page2(Page): @staticmethod def error_message(player: Player, values): import time if time.time() < player.page_pass_time: return "You cannot pass this page yet." class Page3(Page): pass page_sequence = [Page1, Page2, Page3] progress_bar / Page1.html From otree - snippets {{block content}} {{include_sibling 'progress.html'}} {{next_button}} {{endblock}} progress_bar / Page2.html From otree - snippets {{block content}} {{include_sibling 'progress.html'}} {{next_button}} {{endblock}} progress_bar / __init__.py From otree - snippets from otree.api import * doc = """ All you need is a participant field called 'progress' then keep adding 1 to it. """ class C(BaseConstants): NAME_IN_URL = 'progress_bar' PLAYERS_PER_GROUP = None NUM_ROUNDS = 5 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for player in subsession.get_players(): participant = player.participant participant.progress = 1 class Group(BaseGroup): pass class Player(BasePlayer): pass class Page1(Page): @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant # remember to add 'progress' to PARTICIPANT_FIELDS. participant.progress += 1 class Page2(Page): @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant # progress can be defined in different ways, not only by page number # (especially if pages get skipped) # so feel free to do things like: # - incrementing by more than 1: # participant.progress += 2 # - setting to a specific valueInput: # participant.progress = 8 participant.progress += 1 page_sequence = [Page1, Page2] progress_bar / progress.html From otree - snippets < !-- Simplest way to calculate the "max" is to run through the experiment once and then see what participant.progress is at the very end, then plug that in here. if you want a prettier progress bar, you can use Bootstrap's. --> < p > < label > Step {{participant.progress}} of 10 < progress value = "{{ participant.progress }}" max = "10" > < / progress > < / label > < / p > random_task_order / TaskA.html From otree - snippets {{block title}} Task A {{endblock}} {{block content}} {{next_button}} {{endblock}} random_task_order / TaskC.html From otree - snippets {{block title}} Task C {{endblock}} {{block content}} {{next_button}} {{endblock}} random_task_order / __init__.py From otree - snippets import random from otree.api import * doc = """ For each participant, randomize the order of tasks A, B, and C. Task B has 2 pages, which are always shown in the same order. The page_sequence contains all tasks; in each round we show a randomly determined subset of pages. """ class C(BaseConstants): NAME_IN_URL = 'random_task_order' PLAYERS_PER_GROUP = None TASKS = ['A', 'B', 'C'] NUM_ROUNDS = len(TASKS) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # FUNCTIONS def creating_session(subsession: Subsession): if subsession.round_number == 1: for p in subsession.get_players(): round_numbers = list(range(1, C.NUM_ROUNDS + 1)) random.shuffle(round_numbers) task_rounds = dict(zip(C.TASKS, round_numbers)) # print('player', p.id_in_subsession) # print('task_rounds is', task_rounds) p.participant.task_rounds = task_rounds # PAGES class TaskA(Page): @staticmethod def is_displayed(player: Player): participant = player.participant return player.round_number == participant.task_rounds['A'] class TaskB1(Page): @staticmethod def is_displayed(player: Player): participant = player.participant return player.round_number == participant.task_rounds['B'] class TaskB2(Page): @staticmethod def is_displayed(player: Player): participant = player.participant return player.round_number == participant.task_rounds['B'] class TaskC(Page): @staticmethod def is_displayed(player: Player): participant = player.participant return player.round_number == participant.task_rounds['C'] page_sequence = [ TaskA, TaskB1, TaskB2, TaskC, ] random_task_order / TaskB1.html From otree - snippets {{block title}} Task B, Page 1 {{endblock}} {{block content}} {{next_button}} {{endblock}} random_task_order / TaskB2.html From otree - snippets {{block title}} Task B, Page 2 {{endblock}} {{block content}} {{next_button}} {{endblock}} treatments_from_spreadsheet / __init__.py From otree - snippets from otree.api import * doc = """ Reading treatment parameters from a CSV spreadsheet """ class C(BaseConstants): NAME_IN_URL = 'treatments_from_spreadsheet' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): import csv f = open(__name__ + '/treatments.csv', encoding='utf-8-sig') rows = list(csv.DictReader(f)) players = subsession.get_players() for i in range(len(players)): row = rows[i] player = players[i] # CSV contains all data in string form, so we need to convert # to the correct data type, e.g. '1' -> 1 -> True. player.time_pressure = bool(int(row['time_pressure'])) player.high_tax = bool(int(row['high_tax'])) player.endowment = cu(row['endowment']) player.color = row['color'] class Group(BaseGroup): pass class Player(BasePlayer): time_pressure = models.BooleanField() endowment = models.CurrencyField() high_tax = models.BooleanField() color = models.StringField() class MyPage(Page): pass page_sequence = [MyPage] treatments_from_spreadsheet / MyPage.html From otree - snippets {{block content}} < p > < i > Look in the admin "data" tab to see the treatments that were assigned. < / i > < / p > {{endblock}} pass_data_between_apps_part1 / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'pass_data_between_apps1' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): language = models.StringField(label='What is your main language?') # PAGES class MyPage(Page): form_model = 'player' form_fields = ['language'] @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant # in settings.py need to add 'language' to PARTICIPANT_FIELDS. participant.language = player.language page_sequence = [MyPage] pass_data_between_apps_part1 / MyPage.html From otree - snippets {{block title}} App 1 {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} gbat_keep_same_groups_part1 / __init__.py From otree - snippets from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'gbat_keep_same_groups' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class GBATWait(WaitPage): group_by_arrival_time = True @staticmethod def after_all_players_arrive(group: Group): # save each participant's current group ID so it can be # accessed in the next app. for p in group.get_players(): participant = p.participant participant.past_group_id = group.id class MyPage(Page): pass page_sequence = [GBATWait, MyPage] gbat_keep_same_groups_part1 / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > < i > group_by_arrival_time has paired you with a partner. < / i > < / p > {{next_button}} {{endblock}} gbat_keep_same_groups_part2 / __init__.py From otree - snippets from otree.api import * doc = """ Preserve same groups as a previous app that used group_by_arrival time. """ class C(BaseConstants): NAME_IN_URL = 'gbat_keep_same_groups_part2' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def group_by_arrival_time_method(subsession: Subsession, waiting_players): # we now place users into different baskets, according to their group in the previous app. # the dict 'd' will contain all these baskets. d = {} for p in waiting_players: group_id = p.participant.past_group_id if group_id not in d: # since 'd' is initially empty, we need to initialize an empty list (basket) # each time we see a new group ID. d[group_id] = [] players_in_my_group = d[group_id] players_in_my_group.append(p) if len(players_in_my_group) == 2: return players_in_my_group # print('d is', d) class Group(BaseGroup): pass class Player(BasePlayer): pass class GBATWait(WaitPage): group_by_arrival_time = True class MyPage(Page): pass page_sequence = [GBATWait, MyPage] gbat_keep_same_groups_part2 / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > < i > This is the next app.Again you have been paired with the same partner.< / i > < / p > {{next_button}} {{endblock}} appcopy2 / __init__.py From otree - snippets from appcopy1 import * class C(C): NAME_IN_URL = 'appcopy2' # need to copy/paste Subsession/Group/Player classes from appcopy1 class Subsession(BaseSubsession): pass class Group(BaseGroup): aaa = models.IntegerField() class Player(BasePlayer): bbb = models.IntegerField() questions_from_csv_complex / __init__.py From otree - snippets from otree.api import * doc = """ Read quiz questions from a CSV (complex version). See also the 'simple' version. It would be much simpler to implement this using rounds (1 question per round), as is done in the 'simple' version; however, this approach has faster gameplay since it's all done in 1 page, and leads to a more compact data export. Consider using this version if you have many questions or if speed is a high priority. """ class C(BaseConstants): NAME_IN_URL = 'questions_from_csv_complex' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 def read_csv(): import csv import random f = open(__name__ + '/stimuli.csv', encoding='utf-8-sig') rows = list(csv.DictReader(f)) random.shuffle(rows) return rows class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): stimuli = read_csv() p.num_trials = len(stimuli) for stim in stimuli: # print('stim is', stim) # ** is the Python operator to unpack the dict Trial.create(player=p, **stim) class Group(BaseGroup): pass class Player(BasePlayer): num_correct = models.IntegerField(initial=0) raw_responses = models.LongStringField() class Trial(ExtraModel): player = models.Link(Player) question = models.StringField() optionA = models.StringField() optionB = models.StringField() optionC = models.StringField() solution = models.StringField() choice = models.StringField() is_correct = models.BooleanField() def to_dict(trial: Trial): return dict( question=trial.question, optionA=trial.optionA, optionB=trial.optionB, optionC=trial.optionC, id=trial.id, ) # PAGES class Stimuli(Page): form_model = 'player' form_fields = ['raw_responses'] @staticmethod def js_vars(player: Player): stimuli = [to_dict(trial) for trial in Trial.filter(player=player)] return dict(trials=stimuli) @staticmethod def before_next_page(player: Player, timeout_happened): import json responses = json.loads(player.raw_responses) for trial in Trial.filter(player=player): # have to use str() because Javascript implicitly converts keys to strings trial.choice = responses[str(trial.id)] trial.is_correct = trial.choice == trial.solution # convert True/False to 1/0 player.num_correct += int(trial.is_correct) # don't need it anymore player.raw_responses = '' class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(trials=Trial.filter(player=player)) page_sequence = [Stimuli, Results] def custom_export(players): yield ['participant', 'question', 'choice', 'is_correct'] for player in players: participant = player.participant trials = Trial.filter(player=player) for t in trials: yield [participant.code, t.question, t.choice, t.is_correct] questions_from_csv_complex / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > You gave {{player.num_correct}} correct answers. < / p > < table class ="table" > < tr > < th > question < / th > < th > optionA < / th > < th > optionB < / th > < th > optionC < / th > < th > Your choice < / th > < th > solution < / th > < th > correct? < / th > < / tr > {{ for trial in trials}} < tr > < td > {{trial.question}} < / td > < td > {{trial.optionA}} < / td > < td > {{trial.optionB}} < / td > < td > {{trial.optionC}} < / td > < td > {{trial.choice}} < / td > < td > {{trial.solution}} < / td > < td > {{trial.is_correct}} < / td > < / tr > {{endfor}} < / table > {{endblock}} questions_from_csv_complex / Stimuli.html From otree - snippets {{block title}} {{endblock}} {{block content}} < p id = "question" > < / p > < div > < button type = "button" onclick = "recordResponse(this)" value = "A" id = "optionA" > < / button > < button type = "button" onclick = "recordResponse(this)" value = "B" id = "optionB" > < / button > < button type = "button" onclick = "recordResponse(this)" value = "C" id = "optionC" > < / button > < / div > < input type = "hidden" name = "raw_responses" id = "raw_responses" > < script > let responses = {} let trialIndex = 0; let trials = js_vars.trials; function updateUI() { for (let item of['question', 'optionA', 'optionB', 'optionC']) { document.getElementById(item).innerText = trials[trialIndex][item]; } } function recordResponse(btn) { let trialId = trials[trialIndex].id; responses[trialId] = btn.value; trialIndex + +; if (trialIndex === trials.length) { document.getElementById('raw_responses').value = JSON.stringify(responses) document.getElementById('form').submit(); } else { updateUI(); } } updateUI(); < / script > {{endblock}} multi_select_complex / __init__.py From otree - snippets from otree.api import * doc = """ Question that lets you select multiple options (multi-select, multiple choice / multiple answer) The difference is that this one lets you customize the label of each checkbox, and requires at least 1 to be selected. """ class C(BaseConstants): NAME_IN_URL = 'multi_select_complex' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 LANGUAGES = [ dict(name='english', label="I speak English"), dict(name='french', label="Je parle français"), dict(name='spanish', label="Hablo español"), dict(name='finnish', label="Puhun suomea"), ] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): english = models.BooleanField(blank=True) french = models.BooleanField(blank=True) spanish = models.BooleanField(blank=True) finnish = models.BooleanField(blank=True) # PAGES class MyPage(Page): form_model = 'player' @staticmethod def get_form_fields(player: Player): return [lang['name'] for lang in C.LANGUAGES] @staticmethod def error_message(player: Player, values): # print('values is', values) num_selected = 0 for lang in C.LANGUAGES: if values[lang['name']]: num_selected += 1 if num_selected < 1: return "You must select at least 1 language." page_sequence = [MyPage] multi_select_complex / MyPage.html From otree - snippets {{block content}} < p > What languages do you speak? Select all that apply. < / p > {{ for field in C.LANGUAGES}} < label > < input type = "checkbox" name = "{{ field.name }}" value = "1" > {{field.label}} < / label > < br > {{endfor}} < p > {{next_button}} < / p > {{endblock}} factorial_treatments / __init__.py From otree - snippets from otree.api import * doc = """Randomize multiple factors in a balanced way""" class C(BaseConstants): NAME_IN_URL = 'randomize_cross_product' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession): import itertools treatments = itertools.cycle( itertools.product([True, False], [True, False], [100, 200, 300]) ) for p in subsession.get_players(): treatment = next(treatments) # print('treatment is', treatment) p.time_pressure = treatment[0] p.high_tax = treatment[1] p.endowment = treatment[2] class Group(BaseGroup): pass class Player(BasePlayer): time_pressure = models.BooleanField() high_tax = models.BooleanField() endowment = models.CurrencyField() class MyPage(Page): pass page_sequence = [MyPage] factorial_treatments / MyPage.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > < i > Check the admin 'Data' tab to see the results of the randomization < / i > < / p > {{endblock}} random_question_order / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'random_question_order' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): aaa = models.BooleanField() bbb = models.BooleanField() ccc = models.StringField() ddd = models.IntegerField() # PAGES class MyPage(Page): form_model = 'player' @staticmethod def get_form_fields(player: Player): import random form_fields = ['aaa', 'bbb', 'ccc', 'ddd'] random.shuffle(form_fields) return form_fields page_sequence = [MyPage] random_question_order / MyPage.html From otree - snippets {{block title}} Answer these questions {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} bmi_calculator / __init__.py From otree - snippets from otree.api import * doc = """ Basic single-player game (BMI calculator) """ class C(BaseConstants): NAME_IN_URL = 'bmi_calculator' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): weight_kg = models.IntegerField(label="Weight (in kg)") height_cm = models.IntegerField(label="Height (in cm)") bmi = models.FloatField() # PAGES class MyPage(Page): form_model = 'player' form_fields = ['weight_kg', 'height_cm'] @staticmethod def before_next_page(player: Player, timeout_happened): bmi = player.weight_kg / ((player.height_cm / 100) ** 2) player.bmi = round(bmi, 1) class Results(Page): pass page_sequence = [MyPage, Results] bmi_calculator / Results.html From otree - snippets {{block content}} < p > Your BMI is {{player.bmi}}. < / p > {{endblock}} bmi_calculator / MyPage.html From otree - snippets {{block title}} BMI(Body Mass Index) calculator {{endblock}} {{block content}} {{formfields}} {{next_button}} {{endblock}} wait_for_specific_people / WaitForSelected.html From otree - snippets {{block title}} Waiting {{endblock}} {{block content}} < progress > < / progress > < p > Waiting for players: < span id = "wait_for_ids" > < / span > < / p > < script > function liveRecv(data) { console.log('data', data) if (data.finished) { document.getElementById("form").submit(); } else { document.getElementById('wait_for_ids').innerText = data.not_arrived_yet; } } document.addEventListener("DOMContentLoaded", (event) = > { liveSend({}); }); < / script > {{endblock}} wait_for_specific_people / __init__.py From otree - snippets from otree.api import * doc = """ Wait only for specific people """ class C(BaseConstants): NAME_IN_URL = 'wait_for_specific_people' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): session = subsession.session import random session.wait_for_ids = set() session.arrived_ids = set() for p in subsession.get_players(): # we just determine it randomly here. # in your game, you should replace it with your desired logic. selected = random.choice([False, True]) p.selected_for_waitpage = selected if selected: session.wait_for_ids.add(p.id_in_subsession) class Group(BaseGroup): pass class Player(BasePlayer): selected_for_waitpage = models.BooleanField() class Intro(Page): pass class WaitForSelected(Page): @staticmethod def is_displayed(player: Player): return player.selected_for_waitpage @staticmethod def live_method(player: Player, data): session = player.session session.arrived_ids.add(player.id_in_subsession) not_arrived_yet = session.wait_for_ids - session.arrived_ids if not_arrived_yet: return {0: dict(not_arrived_yet=list(not_arrived_yet))} return {0: dict(finished=True)} @staticmethod def error_message(player: Player, values): session = player.session if session.arrived_ids != session.wait_for_ids: return "Page somehow proceeded before all players are ready" class Results(Page): pass page_sequence = [Intro, WaitForSelected, Results] wait_for_specific_people / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > < i > Next page content would go here... < / i > < / p > {{next_button}} {{endblock}} wait_for_specific_people / Intro.html From otree - snippets {{block title}} Intro {{endblock}} {{block content}} < p > This app demonstrates how to have a waiting page that just waits for certain people to arrive before proceeding. You can make it any subset of participants: for example, just the participants you marked as being online currently, or just those who gave a specific answer to a question. < / p > < p > In this demo, the players were randomly selected as: {{session.wait_for_ids}}. < / p > < p > You are player {{player.id_in_subsession}}, so you {{ if player.selected_for_waitpage}} must wait on {{ else}} can skip {{endif}} the following wait page. < / p > < p > Click next. < / p > {{next_button}} {{endblock}} practice_rounds / __init__.py From otree - snippets from otree.api import * doc = """Practice rounds""" class C(BaseConstants): NAME_IN_URL = 'practice_rounds' PLAYERS_PER_GROUP = None NUM_PRACTICE_ROUNDS = 2 NUM_REAL_ROUNDS = 10 NUM_ROUNDS = NUM_PRACTICE_ROUNDS + NUM_REAL_ROUNDS class Subsession(BaseSubsession): is_practice_round = models.BooleanField() real_round_number = models.IntegerField() def creating_session(subsession: Subsession): # In Python, 'a <= b' produces either True or False. subsession.is_practice_round = ( subsession.round_number <= C.NUM_PRACTICE_ROUNDS ) if not subsession.is_practice_round: subsession.real_round_number = ( subsession.round_number - C.NUM_PRACTICE_ROUNDS ) class Group(BaseGroup): pass class Player(BasePlayer): response = models.IntegerField() solution = models.IntegerField() is_correct = models.BooleanField() class Play(Page): form_model = 'player' form_fields = ['response'] @staticmethod def before_next_page(player: Player, timeout_happened): # the **2 is just an example used in this game (squaring a number) player.solution = player.round_number ** 2 player.is_correct = player.response == player.solution class PracticeFeedback(Page): @staticmethod def is_displayed(player: Player): subsession = player.subsession return subsession.is_practice_round class Results(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): score = 0 for p in player.in_rounds( C.NUM_PRACTICE_ROUNDS + 1, C.NUM_ROUNDS ): score += p.is_correct return dict(score=score) page_sequence = [Play, PracticeFeedback, Results] practice_rounds / Results.html From otree - snippets {{block title}} Results {{endblock}} {{block content}} < p > Your got {{score}} answers correct. < / p > {{endblock}} practice_rounds / PracticeFeedback.html From otree - snippets {{block title}} Practice feedback {{endblock}} {{block content}} {{ if player.is_correct}} < p > You got the practice question correct! < / p > {{ else}} < p > You answered {{player.response}} but the correct answer was {{player.solution}}. < / p > {{endif}} < p > Once the real rounds start, you won 't see this feedback page anymore.

{{next_button}} {{endblock}} practice_rounds / Play.html From otree - snippets {{block title}} {{ if subsession.is_practice_round}} Practice round {{subsession.round_number}} of {{C.NUM_PRACTICE_ROUNDS}} {{ else}} Round {{subsession.real_round_number}} of {{C.NUM_REAL_ROUNDS}} {{endif}} {{endblock}} {{block content}} < p > Math question: what is {{player.round_number}} squared? < / p > {{formfields}} {{next_button}} {{endblock}} pay_random_app_multi_player / __init__.py From otree - snippets from otree.api import * doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'pay_random_app1' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): # initialize an empty dict to store how much they made in each app p.participant.app_payoffs = {} class Group(BaseGroup): pass class Player(BasePlayer): potential_payoff = models.CurrencyField() # PAGES class MyPage(Page): pass class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): """ In multiplayer games, payoffs are typically set in after_all_players_arrive, so that's what we demonstrate here. """ import random for p in group.get_players(): participant = p.participant potential_payoff = random.randint(100, 200) p.potential_payoff = potential_payoff # __name__ is a magic variable that contains the name of the current app participant.app_payoffs[__name__] = potential_payoff class Results(Page): pass page_sequence = [MyPage, ResultsWaitPage, Results] pay_random_app_multi_player / Results.html From otree - snippets {{block title}} App 1 Results {{endblock}} {{block content}} < p > Your payoff in this app is {{player.potential_payoff}}. < / p > {{next_button}} {{endblock}} pay_random_app_multi_player / MyPage.html From otree - snippets {{block title}} App 1 {{endblock}} {{block content}} < p > < i > Your game would normally go here.In this case, your payoff will be determined randomly. < / i > < / p > {{next_button}} {{endblock}} supergames / NewSupergame.html From otree - snippets {{block title}} Supergame {{subsession.sg}} {{endblock}} {{block content}} < p > This page is only shown at the beginning of a supergame... < / p > {{next_button}} {{endblock}} chat_with_experimenter / papercups.html From otree - snippets < script > window.Papercups = { config: { accountId: "5ee2437e-b9e9-4348-8e1c-483959b1d826", title: "Welcome to our experiment", subtitle: "Ask us anything in the chat window below", primaryColor: "#1890ff", greeting: "", awayMessage: "", newMessagePlaceholder: "Start typing...", showAgentAvailability: false, agentAvailableText: "We're online right now!", agentUnavailableText: "We're away at the moment.", requireEmailUpfront: false, iconVariant: "outlined", // note: you need to set up your own Papercups chat server(quite easy). baseUrl: "https://otree-papercups.herokuapp.com", customer: { name: '{{participant.code}}', external_id: '{{participant.code}}', } }, }; < / script > < script type = "text/javascript" async defer src = "https://otree-papercups.herokuapp.com/widget.js" > < / script > gbat_treatments / MyPage.html From otree - snippets {{block content}} Your group is in the {{ if group.treatment}} treatment {{ else}} control {{endif}} cohort. {{endblock}} supergames / __init__.py From otree - snippets from otree.api import * doc = """ Supergames consisting of multiple rounds each """ def cumsum(lst): total = 0 new = [] for ele in lst: total += ele new.append(total) return new class C(BaseConstants): NAME_IN_URL = 'supergames' PLAYERS_PER_GROUP = None # first supergame lasts 2 rounds, second supergame lasts 3 rounds, etc... ROUNDS_PER_SG = [2, 3, 4, 5] SG_ENDS = cumsum(ROUNDS_PER_SG) # print('SG_ENDS is', SG_ENDS) NUM_ROUNDS = sum(ROUNDS_PER_SG) class Subsession(BaseSubsession): sg = models.IntegerField() period = models.IntegerField() is_last_period = models.BooleanField() def creating_session(subsession: Subsession): if subsession.round_number == 1: sg = 1 period = 1 # loop over all subsessions for ss in subsession.in_rounds(1, C.NUM_ROUNDS): ss.sg = sg ss.period = period # 'in' gives you a bool. for example: 5 in [1, 5, 6] # => True is_last_period = ss.round_number in C.SG_ENDS ss.is_last_period = is_last_period if is_last_period: sg += 1 period = 1 else: period += 1 class Group(BaseGroup): pass class Player(BasePlayer): pass class NewSupergame(Page): @staticmethod def is_displayed(player: Player): subsession = player.subsession return subsession.period == 1 class Play(Page): pass page_sequence = [NewSupergame, Play] supergames / Play.html From otree - snippets {{block title}} Supergame {{subsession.sg}}, period {{subsession.period}} {{endblock}} {{block content}} < p > < i > Your game goes here... < / i > < / p > {{next_button}} {{endblock}} show_other_players_payoffs / __init__.py From otree - snippets from otree.api import * class C(BaseConstants): NAME_IN_URL = 'show_other_players_payoffs' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(others=player.get_others_in_group()) page_sequence = [Results] show_other_players_payoffs / Results.html From otree - snippets {{block title}} Page title {{endblock}} {{block content}} < p > Your payoff is {{player.payoff}}. < / p > < p > Here are the other players ' payoffs:

< table > {{ for other in others}} < tr > < td > Player {{other.id_in_group}} < / td > < td > {{other.payoff}} < / td > < / tr > {{endfor}} < / table > {{endblock}} getattr_setattr / Page1.html From otree - snippets {{block content}} < p > Enter 10 random numbers from 1 to 100. < / p > {{formfields}} {{next_button}} {{endblock}} getattr_setattr / Page2.html From otree - snippets {{block content}} {{formfields}} {{next_button}} {{endblock}} getattr_setattr / __init__.py From otree - snippets from otree.api import * doc = """ Using getattr() and setattr() to access numbered fields, e.g. player.num1, player.num2, ..., player.num10, without writing repetitive if-statements. NOTE: having numbered fields is often not the best or easiest design. For example, let's say you have fields like this: num1 = models.IntegerField() num2 = models.IntegerField() ... num10 = models.IntegerField() If you don't need to put them in a form, then you can replace this simply with a list in a participant field, since they can be more easily accessed by number, e.g. participant.my_numbers[5] If you have many numbered fields, like more than 20, you should consider using ExtraModel. Participant fields and ExtraModel also have the advantage that you don't need to know in advance exactly how many you will have. """ class C(BaseConstants): NAME_IN_URL = 'getattr_setattr' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 NUMBERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): num1 = models.IntegerField() num2 = models.IntegerField() num3 = models.IntegerField() num4 = models.IntegerField() num5 = models.IntegerField() num6 = models.IntegerField() num7 = models.IntegerField() num8 = models.IntegerField() num9 = models.IntegerField() num10 = models.IntegerField() chosen_number = models.IntegerField( choices=C.NUMBERS, label="Choose a random number from 1 to 10" ) class Page1(Page): form_model = 'player' form_fields = ['num{}'.format(n) for n in C.NUMBERS] class Page2(Page): form_model = 'player' form_fields = ['chosen_number'] class Results(Page): @staticmethod def vars_for_template(player: Player): # if chosen_number was 7, this will give you player.num7 field_name = 'num{}'.format(player.chosen_number) chosen_value = getattr(player, field_name) player.payoff = chosen_value # if chosen number was 7, this gives you # player.num1 + player.num2 + ... + player.num7 sum_to_n = sum( getattr(player, 'num{}'.format(n)) for n in range(1, player.chosen_number + 1) ) return dict(chosen_value=chosen_value, sum_to_n=sum_to_n) page_sequence = [Page1, Page2, Results] getattr_setattr / Results.html From otree - snippets {{block content}} < p > You chose number {{player.chosen_number}}. The random number in that field was {{chosen_value}}. Therefore, your payoff is {{player.payoff}}. By the way, the sum of all numbers from num1 to num {{player.chosen_number}} was {{sum_to_n}}. < / p > {{endblock}} survey / CognitiveReflectionTest.html From otree - demo {{block title}}Survey {{endblock}} {{block content}} < p > Please answer the following questions. < / p > {{formfields}} {{next_button}} {{endblock}} matching_pennies / Choice.html From otree - demo {{block title}}Round {{subsession.round_number}} of {{C.NUM_ROUNDS}} {{endblock}} {{block content}} < h4 > Instructions < / h4 > < p > This is a matching pennies game. Player 1 is the 'Mismatcher' and wins if the choices mismatch; Player 2 is the 'Matcher' and wins if they match. < / p > < p > At the end, a random round will be chosen for payment. < / p > < p > < h4 > Round history < / h4 > < table class ="table" > < tr > < th > Round < / th > < th > Player and outcome < / th > < / tr > {{ for p in player_in_previous_rounds}} < tr > < td > {{p.round_number}} < / td > < td > You were the {{p.role}} and {{ if p.is_winner}} won {{ else}} lost {{endif}} < / td > < / tr > {{endfor}} < / table > < p > In this round, you are the {{player.role}}. < / p > {{formfields}} {{next_button}} {{endblock}} matching_pennies / __init__.py From otree - demo from otree.api import * doc = """ A demo of how rounds work in oTree, in the context of 'matching pennies' """ class C(BaseConstants): NAME_IN_URL = 'matching_pennies' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 4 STAKES = cu(100) MATCHER_ROLE = 'Matcher' MISMATCHER_ROLE = 'Mismatcher' class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): penny_side = models.StringField( choices=[['Heads', 'Heads'], ['Tails', 'Tails']], widget=widgets.RadioSelect, label="I choose:", ) is_winner = models.BooleanField() # FUNCTIONS def creating_session(subsession: Subsession): session = subsession.session import random if subsession.round_number == 1: paying_round = random.randint(1, C.NUM_ROUNDS) session.vars['paying_round'] = paying_round if subsession.round_number == 3: # reverse the roles matrix = subsession.get_group_matrix() for row in matrix: row.reverse() subsession.set_group_matrix(matrix) if subsession.round_number > 3: subsession.group_like_round(3) def set_payoffs(group: Group): subsession = group.subsession session = group.session p1 = group.get_player_by_id(1) p2 = group.get_player_by_id(2) for p in [p1, p2]: is_matcher = p.role == C.MATCHER_ROLE p.is_winner = (p1.penny_side == p2.penny_side) == is_matcher if subsession.round_number == session.vars['paying_round'] and p.is_winner: p.payoff = C.STAKES else: p.payoff = cu(0) # PAGES class Choice(Page): form_model = 'player' form_fields = ['penny_side'] @staticmethod def vars_for_template(player: Player): return dict(player_in_previous_rounds=player.in_previous_rounds()) class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class ResultsSummary(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): session = player.session player_in_all_rounds = player.in_all_rounds() return dict( total_payoff=sum([p.payoff for p in player_in_all_rounds]), paying_round=session.vars['paying_round'], player_in_all_rounds=player_in_all_rounds, ) page_sequence = [Choice, ResultsWaitPage, ResultsSummary] dictator / __init__.py From otree - demo from otree.api import * doc = """ One player decides how to divide a certain amount between himself and the other player. See: Kahneman, Daniel, Jack L. Knetsch, and Richard H. Thaler. "Fairness and the assumptions of economics." Journal of business (1986): S285-S300. """ class C(BaseConstants): NAME_IN_URL = 'dictator' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 # Initial amount allocated to the dictator ENDOWMENT = cu(100) class Subsession(BaseSubsession): pass class Group(BaseGroup): kept = models.CurrencyField( doc="""Amount dictator decided to keep for himself""", min=0, max=C.ENDOWMENT, label="I will keep", ) class Player(BasePlayer): pass # FUNCTIONS def set_payoffs(group: Group): p1 = group.get_player_by_id(1) p2 = group.get_player_by_id(2) p1.payoff = group.kept p2.payoff = C.ENDOWMENT - group.kept # PAGES class Introduction(Page): pass class Offer(Page): form_model = 'group' form_fields = ['kept'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): group = player.group return dict(offer=C.ENDOWMENT - group.kept) page_sequence = [Introduction, Offer, ResultsWaitPage, Results] dictator / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < p > {{ if player.id_in_group == 1}} You decided to keep < strong > {{group.kept}} < / strong > for yourself. {{ else}} Participant 1 decided to keep < strong > {{group.kept}} < / strong >, so you got < strong > {{offer}} < / strong >. {{endif}} {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} dictator / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You will be paired randomly and anonymously with another participant. In this study, one of you will be Participant 1 and the other Participant 2. Prior to making a decision, you will learn your role, which will be randomly assigned. < / p > < p > There is {{C.ENDOWMENT}} to split.Participant 1 will decide how much she or he will retain.Then the rest will go to Participant 2. < / p > < / div > < / div > dictator / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} dictator / Offer.html From otree - demo {{block title}}Your Decision {{endblock}} {{block content}} < p > You are < strong > Participant 1 < / strong >. Please decide how much of the {{C.ENDOWMENT}} you will keep for yourself. < / p > {{formfields}} {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} trust / SendBack.html From otree - demo {{block title}}Your Choice {{endblock}} {{block content}} < p > You are Participant B. Participant A sent you {{group.sent_amount}} and you received {{tripled_amount}}. Now you have {{tripled_amount}}. How much will you send to participant A? < / p > {{formfields}} < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} trust / Send.html From otree - demo {{block title}}Your Choice {{endblock}} {{block content}} < p > You are Participant A.Now you have {{C.ENDOWMENT}}.How much will you send to participant B? < / p > {{formfields}} < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} trust / __init__.py From otree - demo from otree.api import * doc = """ This is a standard 2-player trust game where the amount sent by player 1 gets tripled. The trust game was first proposed by Berg, Dickhaut, and McCabe (1995) . """ class C(BaseConstants): NAME_IN_URL = 'trust' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 # Initial amount allocated to each player ENDOWMENT = cu(100) MULTIPLIER = 3 class Subsession(BaseSubsession): pass class Group(BaseGroup): sent_amount = models.CurrencyField( min=0, max=C.ENDOWMENT, doc="""Amount sent by P1""", label="Please enter an amount from 0 to 100:", ) sent_back_amount = models.CurrencyField(doc="""Amount sent back by P2""", min=cu(0)) class Player(BasePlayer): pass # FUNCTIONS def sent_back_amount_max(group: Group): return group.sent_amount * C.MULTIPLIER def set_payoffs(group: Group): p1 = group.get_player_by_id(1) p2 = group.get_player_by_id(2) p1.payoff = C.ENDOWMENT - group.sent_amount + group.sent_back_amount p2.payoff = group.sent_amount * C.MULTIPLIER - group.sent_back_amount # PAGES class Introduction(Page): pass class Send(Page): """This page is only for P1 P1 sends amount (all, some, or none) to P2 This amount is tripled by experimenter, i.e if sent amount by P1 is 5, amount received by P2 is 15""" form_model = 'group' form_fields = ['sent_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 class SendBackWaitPage(WaitPage): pass class SendBack(Page): """This page is only for P2 P2 sends back some amount (of the tripled amount received) to P1""" form_model = 'group' form_fields = ['sent_back_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 2 @staticmethod def vars_for_template(player: Player): group = player.group tripled_amount = group.sent_amount * C.MULTIPLIER return dict(tripled_amount=tripled_amount) class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): """This page displays the earnings of each player""" @staticmethod def vars_for_template(player: Player): group = player.group return dict(tripled_amount=group.sent_amount * C.MULTIPLIER) page_sequence = [ Introduction, Send, SendBackWaitPage, SendBack, ResultsWaitPage, Results, ] trust / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} {{ if player.id_in_group == 1}} < p > You chose to send participant B {{group.sent_amount}}. Participant B returned {{group.sent_back_amount}}. < / p > < p > You were initially endowed with {{C.ENDOWMENT}}, chose to send {{group.sent_amount}}, received {{group.sent_back_amount}} thus you now have: {{C.ENDOWMENT}} - {{group.sent_amount}} + {{group.sent_back_amount}} = < strong > {{player.payoff}} < / strong > < / p > {{ else}} < p > Participant A sent you {{group.sent_amount}}. They were tripled so you received {{tripled_amount}}. You chose to return {{group.sent_back_amount}}. < / p > < p > You received {{tripled_amount}}, chose to return {{group.sent_back_amount}} thus you now have: ({{tripled_amount}}) - ({{group.sent_back_amount}}) = < strong > {{player.payoff}} < / strong > < / p > . {{endif}} < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} trust / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You have been randomly and anonymously paired with another participant. One of you will be selected at random to be participant A; the other will be participant B. You will learn whether you are participant A or B prior to making any decision. < / p > < p > To start, participant A receives {{C.ENDOWMENT}}; participant B receives nothing. Participant A can send some or all of his {{C.ENDOWMENT}} to participant B. Before B receives this amount, it will be multiplied by {{C.MULTIPLIER}}.Once B receives the tripled amount he can decide to send some or all of it to A. < / p > < / div > < / div > trust / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} bertrand / __init__.py From otree - demo from otree.api import * doc = """ 2 firms complete in a market by setting prices for homogenous goods. See "Kruse, J. B., Rassenti, S., Reynolds, S. S., & Smith, V. L. (1994). Bertrand-Edgeworth competition in experimental markets. Econometrica: Journal of the Econometric Society, 343-371." """ class C(BaseConstants): PLAYERS_PER_GROUP = 2 NAME_IN_URL = 'bertrand' NUM_ROUNDS = 1 MAXIMUM_PRICE = cu(100) class Subsession(BaseSubsession): pass class Group(BaseGroup): winning_price = models.CurrencyField() class Player(BasePlayer): price = models.CurrencyField( min=0, max=C.MAXIMUM_PRICE, doc="""Price player offers to sell product for""", label="Please enter an amount from 0 to 100 as your price", ) is_winner = models.BooleanField() # FUNCTIONS def set_payoffs(group: Group): import random players = group.get_players() group.winning_price = min([p.price for p in players]) winners = [p for p in players if p.price == group.winning_price] winner = random.choice(winners) for p in players: if p == winner: p.is_winner = True p.payoff = p.price else: p.is_winner = False p.payoff = cu(0) # PAGES class Introduction(Page): pass class Decide(Page): form_model = 'player' form_fields = ['price'] class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): pass page_sequence = [Introduction, Decide, ResultsWaitPage, Results] bertrand / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < table class ="table" > < tr > < th > Your price < / th > < td > {{player.price}} < / td > < / tr > < tr > < th > Lowest price < / th > < td > {{group.winning_price}} < / td > < / tr > < tr > < th > Was your product sold? < / th > < td > {{ if player.is_winner}} Yes {{ else}} No {{endif}} < / td > < / tr > < tr > < th > Your payoff < / th > < td > {{player.payoff}} < / td > < / tr > < / table > {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} bertrand / Decide.html From otree - demo {{block title}}Set Your Price {{endblock}} {{block content}} {{formfields}} < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} bertrand / instructions.html From otree - demo < div class ="instructions well well-lg" style="" > < h3 > Instructions < / h3 > < p > You have been randomly and anonymously paired with another participant. Each of you will represent a firm.Each firm manufactures one unit of the same product at no cost. < / p > < p > Each of you privately sets your price, anything from 0 to {{C.MAXIMUM_PRICE}}. The buyer in the market will always buy one unit of the product at the lower price.In case of a tie, the buyer will buy from one of you at random.Your profit is your price if your product is sold and zero otherwise. < / p > < / div > bertrand / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} volunteer_dilemma / Decision.html From otree - demo {{block title}}Your Choice {{endblock}} {{block content}} {{formfields}} < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} volunteer_dilemma / __init__.py From otree - demo from otree.api import * doc = """ Each player decides if to free ride or to volunteer from which all will benefit. See: Diekmann, A. (1985). Volunteer's dilemma. Journal of Conflict Resolution, 605-610. """ class C(BaseConstants): NAME_IN_URL = 'volunteer_dilemma' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 NUM_OTHER_PLAYERS = PLAYERS_PER_GROUP - 1 # """Payoff for each player if at least one volunteers""" GENERAL_BENEFIT = cu(100) # """Cost incurred by volunteering player""" VOLUNTEER_COST = cu(40) class Subsession(BaseSubsession): pass class Group(BaseGroup): num_volunteers = models.IntegerField() class Player(BasePlayer): volunteer = models.BooleanField( label='Do you wish to volunteer?', doc="""Whether player volunteers""" ) # FUNCTIONS def set_payoffs(group: Group): players = group.get_players() group.num_volunteers = sum([p.volunteer for p in players]) if group.num_volunteers > 0: baseline_amount = C.GENERAL_BENEFIT else: baseline_amount = cu(0) for p in players: p.payoff = baseline_amount if p.volunteer: p.payoff -= C.VOLUNTEER_COST # PAGES class Introduction(Page): pass class Decision(Page): form_model = 'player' form_fields = ['volunteer'] class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): pass page_sequence = [Introduction, Decision, ResultsWaitPage, Results] volunteer_dilemma / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < p > {{ if player.volunteer}} You volunteered.As a result, your payoff is < strong > {{player.payoff}} < / strong >. {{ elif group.num_volunteers > 0}} You did not volunteer but some did.As a result, your payoff is < strong > {{player.payoff}} < / strong >. {{ else}} You did not volunteer and no one did.As a result, your payoff is < strong > {{player.payoff}} < / strong >. {{endif}} < / p > < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} volunteer_dilemma / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You will be grouped randomly and anonymously with another {{C.NUM_OTHER_PLAYERS}} participants. < / p > < p > Each of you decides independently and simultaneously whether you will volunteer or not.If at least one of you volunteers, everyone will get {{C.GENERAL_BENEFIT}}.However, the volunteer(s) will pay {{C.VOLUNTEER_COST}}.If no one volunteers, everyone receives nothing. < / p > < / div > < / div > volunteer_dilemma / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} guess_two_thirds / Guess.html From otree - demo {{block title}}Your Guess {{endblock}} {{block content}} {{ if player.round_number > 1}} < p > Here were the two - thirds - average values in previous rounds: {{two_thirds_avg_history}} < / p > {{endif}} {{formfields}} {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} guess_two_thirds / __init__.py From otree - demo from otree.api import * doc = """ a.k.a. Keynesian beauty contest. Players all guess a number; whoever guesses closest to 2/3 of the average wins. See https://en.wikipedia.org/wiki/Guess_2/3_of_the_average """ class C(BaseConstants): PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 3 NAME_IN_URL = 'guess_two_thirds' JACKPOT = cu(100) GUESS_MAX = 100 class Subsession(BaseSubsession): pass class Group(BaseGroup): two_thirds_avg = models.FloatField() best_guess = models.IntegerField() num_winners = models.IntegerField() class Player(BasePlayer): guess = models.IntegerField( min=0, max=C.GUESS_MAX, label="Please pick a number from 0 to 100:" ) is_winner = models.BooleanField(initial=False) # FUNCTIONS def set_payoffs(group: Group): players = group.get_players() guesses = [p.guess for p in players] two_thirds_avg = (2 / 3) * sum(guesses) / len(players) group.two_thirds_avg = round(two_thirds_avg, 2) group.best_guess = min(guesses, key=lambda guess: abs(guess - group.two_thirds_avg)) winners = [p for p in players if p.guess == group.best_guess] group.num_winners = len(winners) for p in winners: p.is_winner = True p.payoff = C.JACKPOT / group.num_winners def two_thirds_avg_history(group: Group): return [g.two_thirds_avg for g in group.in_previous_rounds()] # PAGES class Introduction(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 class Guess(Page): form_model = 'player' form_fields = ['guess'] @staticmethod def vars_for_template(player: Player): group = player.group return dict(two_thirds_avg_history=two_thirds_avg_history(group)) class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): group = player.group sorted_guesses = sorted(p.guess for p in group.get_players()) return dict(sorted_guesses=sorted_guesses) page_sequence = [Introduction, Guess, ResultsWaitPage, Results] guess_two_thirds / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < p > Here were the numbers guessed: < / p > < p > {{sorted_guesses}} < / p > < p > Two - thirds of the average of these numbers is {{group.two_thirds_avg}}; the closest guess was {{group.best_guess}}. < / p > < p > Your guess was {{player.guess}}. < / p > < p > {{ if player.is_winner}} {{ if group.num_winners > 1}} Therefore, you are one of the {{group.num_winners}} winners who tied for the best guess. {{ else}} Therefore, you win! {{endif}} {{ else}} Therefore, you did not win. {{endif}} Your payoff is {{player.payoff}}. < / p > {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} guess_two_thirds / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You are in a group of {{C.PLAYERS_PER_GROUP}} people. Each of you will be asked to choose a number between 0 and {{C.GUESS_MAX}}. The winner will be the participant whose number is closest to 2 / 3 of the average of all chosen numbers. < / p > < p > The winner will receive {{C.JACKPOT}}. In case of a tie, the {{C.JACKPOT}} will be equally divided among winners. < / p > < p > This game will be played for {{C.NUM_ROUNDS}} rounds. < / div > < / div > guess_two_thirds / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} bargaining / Request.html From otree - demo {{block title}}Request {{endblock}} {{block content}} < p > How much will you demand for yourself? < / p > {{formfields}} < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} bargaining / __init__.py From otree - demo from otree.api import * doc = """ This bargaining game involves 2 players. Each demands for a portion of some available amount. If the sum of demands is no larger than the available amount, both players get demanded portions. Otherwise, both get nothing. """ class C(BaseConstants): NAME_IN_URL = 'bargaining' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 AMOUNT_SHARED = cu(100) class Subsession(BaseSubsession): pass class Group(BaseGroup): total_requests = models.CurrencyField() class Player(BasePlayer): request = models.CurrencyField( doc=""" Amount requested by this player. """, min=0, max=C.AMOUNT_SHARED, label="Please enter an amount from 0 to 100", ) # FUNCTIONS def set_payoffs(group: Group): players = group.get_players() group.total_requests = sum([p.request for p in players]) if group.total_requests <= C.AMOUNT_SHARED: for p in players: p.payoff = p.request else: for p in players: p.payoff = cu(0) def other_player(player: Player): return player.get_others_in_group()[0] # PAGES class Introduction(Page): pass class Request(Page): form_model = 'player' form_fields = ['request'] class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(other_player_request=other_player(player).request) page_sequence = [Introduction, Request, ResultsWaitPage, Results] bargaining / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < table class =table style="width: auto" > < tr > < th > You demanded < / th > < td > {{player.request}} < / td > < / tr > < tr > < th > The other participant demanded < / th > < td > {{other_player_request}} < / td > < / tr > < tr > < th > Sum of your demands < / th > < td > {{group.total_requests}} < / td > < / tr > < tr > < th > Thus you earn < / th > < td > {{player.payoff}} < / td > < / tr > < / table > < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} bargaining / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You have been randomly and anonymously paired with another participant. There is {{C.AMOUNT_SHARED}} for you to divide. Both of you have to simultaneously and independently demand a portion of the {{C.AMOUNT_SHARED}} for yourselves.If the sum of your demands is smaller or equal to {{C.AMOUNT_SHARED}}, both of you get what you demanded.If the sum of your demands is larger than {{C.AMOUNT_SHARED}}, both of you get nothing. < / p > < / div > < / div > bargaining / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} prisoner / Decision.html From otree - demo {{block title}}Your Choice {{endblock}} {{block content}} < div class ="form-group required" > < table class ="table table-bordered text-center" style="width: auto; margin: auto" > < tr > < th colspan = "2" rowspan = "2" > < / th > < th colspan = "2" > The Other Participant < / th > < / tr > < tr > < th > Cooperate < / th > < th > Defect < / th > < / tr > < tr > < th rowspan = "2" > < span > You < / span > < / th > < td > < button name = "cooperate" value = "True" class ="btn btn-primary btn-large" > I will cooperate < / button > < / td > < td > {{C.PAYOFF_B}}, {{C.PAYOFF_B}} < / td > < td > {{C.PAYOFF_D}}, {{C.PAYOFF_A}} < / td > < / tr > < tr > < td > < button name = "cooperate" value = "False" class ="btn btn-primary btn-large" > I will defect < / button > < / td > < td > {{C.PAYOFF_A}}, {{C.PAYOFF_D}} < / td > < td > {{C.PAYOFF_C}}, {{C.PAYOFF_C}} < / td > < / tr > < / table > < / div > < p > Here you can chat with the other participant.< / p > {{chat}} {{include_sibling 'instructions.html'}} {{endblock}} prisoner / __init__.py From otree - demo from otree.api import * doc = """ This is a one-shot "Prisoner's Dilemma". Two players are asked separately whether they want to cooperate or defect. Their choices directly determine the payoffs. """ class C(BaseConstants): NAME_IN_URL = 'prisoner' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 PAYOFF_A = cu(300) PAYOFF_B = cu(200) PAYOFF_C = cu(100) PAYOFF_D = cu(0) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): cooperate = models.BooleanField( choices=[[True, 'Cooperate'], [False, 'Defect']], doc="""This player's decision""", widget=widgets.RadioSelect, ) # FUNCTIONS def set_payoffs(group: Group): for p in group.get_players(): set_payoff(p) def other_player(player: Player): return player.get_others_in_group()[0] def set_payoff(player: Player): payoff_matrix = { (False, True): C.PAYOFF_A, (True, True): C.PAYOFF_B, (False, False): C.PAYOFF_C, (True, False): C.PAYOFF_D, } other = other_player(player) player.payoff = payoff_matrix[(player.cooperate, other.cooperate)] # PAGES class Introduction(Page): timeout_seconds = 100 class Decision(Page): form_model = 'player' form_fields = ['cooperate'] class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): opponent = other_player(player) return dict( opponent=opponent, same_choice=player.cooperate == opponent.cooperate, my_decision=player.field_display('cooperate'), opponent_decision=opponent.field_display('cooperate'), ) page_sequence = [Introduction, Decision, ResultsWaitPage, Results] prisoner / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < p > {{ if same_choice}} Both of you chose to {{my_decision}}. {{ else}} You chose to {{my_decision}} and the other participant chose to {{opponent_decision}}. {{endif}} < / p > < p > As a result, you earned {{player.payoff}}. < / p > {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} prisoner / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > In this study, you will be randomly and anonymously paired with another participant. Each of you simultaneously and privately chooses whether you want to cooperate or defect. Your payoffs will be determined by the choices of both as below: < / p > < p > < i > In each cell, the amount to the left is the payoff for you and to the right for the other participant.< / i > < / p > < table class ='table table-bordered text-center' style = 'width: auto; margin: auto' > < tr > < th colspan = 2 rowspan = 2 > < / th > < th colspan = 2 > The Other Participant < / th > < / tr > < tr > < th > Cooperate < / th > < th > Defect < / th > < / tr > < tr > < th rowspan = 2 > < span style = "transform: rotate(-90deg);" > You < / span > < / th > < th > Cooperate < / th > < td > {{C.PAYOFF_B}}, {{C.PAYOFF_B}} < / td > < td > 0, {{C.PAYOFF_A}} < / td > < / tr > < tr > < th > Defect < / th > < td > {{C.PAYOFF_A}}, 0 < / td > < td > {{C.PAYOFF_C}}, {{C.PAYOFF_C}} < / td > < / tr > < / table > < / div > < / div > prisoner / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} public_goods_simple / __init__.py From otree - demo from otree.api import * class C(BaseConstants): NAME_IN_URL = 'public_goods_simple' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 ENDOWMENT = cu(100) MULTIPLIER = 1.8 class Subsession(BaseSubsession): pass class Group(BaseGroup): total_contribution = models.CurrencyField() individual_share = models.CurrencyField() class Player(BasePlayer): contribution = models.CurrencyField( min=0, max=C.ENDOWMENT, label="How much will you contribute?" ) # FUNCTIONS def set_payoffs(group: Group): players = group.get_players() contributions = [p.contribution for p in players] group.total_contribution = sum(contributions) group.individual_share = ( group.total_contribution * C.MULTIPLIER / C.PLAYERS_PER_GROUP ) for p in players: p.payoff = C.ENDOWMENT - p.contribution + group.individual_share # PAGES class Contribute(Page): form_model = 'player' form_fields = ['contribution'] class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): pass page_sequence = [Contribute, ResultsWaitPage, Results] public_goods_simple / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < p > You started with an endowment of {{C.ENDOWMENT}}, of which you contributed {{player.contribution}}. Your group contributed {{group.total_contribution}}, resulting in an individual share of {{group.individual_share}}. Your profit is therefore {{player.payoff}}. < / p > {{next_button}} {{endblock}} public_goods_simple / Contribute.html From otree - demo {{extends "global/Page.html"}} {{block title}}Contribute {{endblock}} {{block content}} < p > This is a public goods game with {{C.PLAYERS_PER_GROUP}} players per group, an endowment of {{C.ENDOWMENT}}, and an efficiency factor of {{C.MULTIPLIER}}. < / p > {{formfields}} {{next_button}} {{endblock}} traveler_dilemma / __init__.py From otree - demo from otree.api import * doc = """ Kaushik Basu's famous traveler's dilemma ( AER 1994 ). It is a 2-player game. The game is framed as a traveler's dilemma and intended for classroom/teaching use. """ class C(BaseConstants): NAME_IN_URL = 'traveler_dilemma' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 # Player's reward for the lowest claim""" ADJUSTMENT_ABS = cu(2) # Player's deduction for the higher claim # The maximum claim to be requested MAX_AMOUNT = cu(100) # The minimum claim to be requested MIN_AMOUNT = cu(2) class Subsession(BaseSubsession): pass class Group(BaseGroup): lower_claim = models.CurrencyField() class Player(BasePlayer): claim = models.CurrencyField( min=C.MIN_AMOUNT, max=C.MAX_AMOUNT, label='How much will you claim for your antique?', doc=""" Each player's claim """, ) adjustment = models.CurrencyField() # FUNCTIONS def set_payoffs(group: Group): p1, p2 = group.get_players() if p1.claim == p2.claim: group.lower_claim = p1.claim for p in [p1, p2]: p.payoff = group.lower_claim p.adjustment = cu(0) else: if p1.claim < p2.claim: winner = p1 loser = p2 else: winner = p2 loser = p1 group.lower_claim = winner.claim winner.adjustment = C.ADJUSTMENT_ABS loser.adjustment = -C.ADJUSTMENT_ABS winner.payoff = group.lower_claim + winner.adjustment loser.payoff = group.lower_claim + loser.adjustment def other_player(player: Player): return player.get_others_in_group()[0] # PAGES class Introduction(Page): pass class Claim(Page): form_model = 'player' form_fields = ['claim'] class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(other_player_claim=other_player(player).claim) page_sequence = [Introduction, Claim, ResultsWaitPage, Results] traveler_dilemma / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < table class =table style='width: auto' > < tr > < td > You claimed < / td > < td > {{player.claim}} < / td > < / tr > < tr > < td > The other traveler claimed < / td > < td > {{other_player_claim}} < / td > < / tr > < tr > < td > Winning claim(i.e.lower claim) < / td > < td > {{group.lower_claim}} < / td > < / tr > < tr > < td > Your adjustment < / td > < td > {{player.adjustment}} < / td > < / tr > < tr > < td > Thus you receive < / td > < td > {{player.payoff}} < / td > < / tr > < / table > < p > {{next_button}} < / p > {{include_sibling 'instructions.html'}} {{endblock}} traveler_dilemma / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You have been randomly and anonymously paired with another participant. Now please image the following scenario. < / p > < p > You and another traveler(the other participant) just returned from a remote island where both of you bought the same antiques. Unfortunately, you discovered that your airline managed to smash the antiques, as they always do.The airline manager assures you of adequate compensation.Without knowing the true value of your antiques, he offers you the following scheme.Both of you simultaneously and independently make a claim for the value of your own antique (ranging from {{C.MIN_AMOUNT}} to {{C.MAX_AMOUNT}}): < / p > < ul > < li > If both claim the same amount, then this amount will be paid to both. < / li > < li > If you claim different amounts, then the lower amount will be paid to both.Additionally, the one with lower claim will receive a reward of {{C.ADJUSTMENT_ABS}}; the one with higher claim will receive a penalty of {{C.ADJUSTMENT_ABS}}. < / li > < / ul > < / div > < / div > traveler_dilemma / Claim.html From otree - demo {{block title}}Claim {{endblock}} {{block content}} {{formfields}} {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} traveler_dilemma / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} matching_pennies / ResultsSummary.html From otree - demo {{block title}}Final results {{endblock}} {{block content}} < table class ="table" > < tr > < th > Round < / th > < th > Player and outcome < / th > < / tr > {{ for p in player_in_all_rounds}} < tr > < td > {{p.round_number}} < / td > < td > You were the {{p.role}} and {{ if p.is_winner}} won {{ else}} lost {{endif}} < / td > < / tr > {{endfor}} < / table > < p > The paying round was {{paying_round}}. Your total payoff is therefore {{total_payoff}}. < / p > {{endblock}} survey / __init__.py From otree - demo from otree.api import * class C(BaseConstants): NAME_IN_URL = 'survey' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): age = models.IntegerField(label='What is your age?', min=13, max=125) gender = models.StringField( choices=[['Male', 'Male'], ['Female', 'Female']], label='What is your gender?', widget=widgets.RadioSelect, ) crt_bat = models.IntegerField( label=''' A bat and a ball cost 22 dollars in total. The bat costs 20 dollars more than the ball. How many dollars does the ball cost?''' ) crt_widget = models.IntegerField( label=''' If it takes 5 machines 5 minutes to make 5 widgets, how many minutes would it take 100 machines to make 100 widgets? ''' ) crt_lake = models.IntegerField( label=''' In a lake, there is a patch of lily pads. Every day, the patch doubles in size. If it takes 48 days for the patch to cover the entire lake, how many days would it take for the patch to cover half of the lake? ''' ) # FUNCTIONS # PAGES class Demographics(Page): form_model = 'player' form_fields = ['age', 'gender'] class CognitiveReflectionTest(Page): form_model = 'player' form_fields = ['crt_bat', 'crt_widget', 'crt_lake'] page_sequence = [Demographics, CognitiveReflectionTest] survey / Demographics.html From otree - demo {{block title}}Survey {{endblock}} {{block content}} < p > Please answer the following questions. < / p > {{formfields}} {{next_button}} {{endblock}} payment_info / __init__.py From otree - demo from otree.api import * doc = """ This application provides a webpage instructing participants how to get paid. Examples are given for the lab and Amazon Mechanical Turk (AMT). """ class C(BaseConstants): NAME_IN_URL = 'payment_info' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass # FUNCTIONS # PAGES class PaymentInfo(Page): @staticmethod def vars_for_template(player: Player): participant = player.participant return dict(redemption_code=participant.label or participant.code) page_sequence = [PaymentInfo] payment_info / PaymentInfo.html From otree - demo {{block title}}Thank you {{endblock}} {{block content}} < p > < em > Below are examples of messages that could be displayed for different experimental settings.< / em > < / p > < div class ="panel panel-default" style="margin-bottom:10px" > < div class ="panel-body" > < p > < b > Laboratory: < / b > < / p > < p > Please remain seated until your number is called.Then take your number card, and proceed to the cashier. < / p > < p > < em > Note: For the cashier in the laboratory, oTree can print a list of payments for all participants as a PDF.< / em > < / p > < / div > < / div > < div class ="panel panel-default" style="margin-bottom:10px" > < div class ="panel-body" > < p > < b > Classroom: < / b > < / p > < p > < em > If you want to keep track of how students did, the easiest thing is to assign the starting links to students by name. It is even possible to give each student a single permanent link for a whole semester using Rooms; so no need to waste time in each lecture with handing out new links and keeping track of which student uses which link. Alternatively, you may just give students anonymous links or secret nicknames. < / em > < / p > < / div > < / div > {{endblock}} common_value_auction / __init__.py From otree - demo from otree.api import * doc = """ In a common value auction game, players simultaneously bid on the item being auctioned.
Prior to bidding, they are given an estimate of the actual value of the item. This actual value is revealed after the bidding.
Bids are private. The player with the highest bid wins the auction, but payoff depends on the bid amount and the actual value.
""" class C(BaseConstants): NAME_IN_URL = 'common_value_auction' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 BID_MIN = cu(0) BID_MAX = cu(10) # Error margin for the value estimates shown to the players BID_NOISE = cu(1) class Subsession(BaseSubsession): pass class Group(BaseGroup): item_value = models.CurrencyField( doc="""Common value of the item to be auctioned, random for treatment""" ) highest_bid = models.CurrencyField() class Player(BasePlayer): item_value_estimate = models.CurrencyField( doc="""Estimate of the common value, may be different for each player""" ) bid_amount = models.CurrencyField( min=C.BID_MIN, max=C.BID_MAX, doc="""Amount bidded by the player""", label="Bid amount", ) is_winner = models.BooleanField( initial=False, doc="""Indicates whether the player is the winner""" ) # FUNCTIONS def creating_session(subsession: Subsession): for g in subsession.get_groups(): import random item_value = random.uniform(C.BID_MIN, C.BID_MAX) g.item_value = round(item_value, 1) def set_winner(group: Group): import random players = group.get_players() group.highest_bid = max([p.bid_amount for p in players]) players_with_highest_bid = [p for p in players if p.bid_amount == group.highest_bid] winner = random.choice( players_with_highest_bid ) # if tie, winner is chosen at random winner.is_winner = True for p in players: set_payoff(p) def generate_value_estimate(group: Group): import random estimate = group.item_value + random.uniform(-C.BID_NOISE, C.BID_NOISE) estimate = round(estimate, 1) if estimate < C.BID_MIN: estimate = C.BID_MIN if estimate > C.BID_MAX: estimate = C.BID_MAX return estimate def set_payoff(player: Player): group = player.group if player.is_winner: player.payoff = group.item_value - player.bid_amount if player.payoff < 0: player.payoff = 0 else: player.payoff = 0 # PAGES class Introduction(Page): @staticmethod def before_next_page(player: Player, timeout_happened): group = player.group player.item_value_estimate = generate_value_estimate(group) class Bid(Page): form_model = 'player' form_fields = ['bid_amount'] class ResultsWaitPage(WaitPage): after_all_players_arrive = set_winner class Results(Page): @staticmethod def vars_for_template(player: Player): group = player.group return dict(is_greedy=group.item_value - player.bid_amount < 0) page_sequence = [Introduction, Bid, ResultsWaitPage, Results] common_value_auction / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < p > {{ if player.is_winner}} You won the auction! {{ if is_greedy}} However, your bid amount was higher than the actual value of the item.Your payoff is therefore zero. {{ elif player.payoff == 0}} Your payoff, however, is zero. {{endif}} {{ else}} You did not win the auction. {{endif}} < / p > < table class ="table" style="width:400px" > < tr > < th > Your bid < / th > < th > Winning bid < / th > < th > Actual value < / th > < th > Your payoff < / th > < / tr > < tr > < td > {{player.bid_amount}} < / td > < td > {{group.highest_bid}} < / td > < td > {{group.item_value}} < / td > < td > {{player.payoff}} < / td > < / tr > < / table > {{next_button}} {{endblock}} common_value_auction / Bid.html From otree - demo {{block title}}Bid {{endblock}} {{block content}} < p > The value of the item is estimated to be {{player.item_value_estimate}}. This estimate may deviate from the actual value by at most {{C.BID_NOISE}}. < / p > < p > Please make your bid now.The amount can be between {{C.BID_MIN}} and {{C.BID_MAX}}, inclusive. < / p > {{formfields}} {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} common_value_auction / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You have been randomly and anonymously grouped with other players. All players see these same instructions. < / p > < p > Your task will be to bid for an item that is being auctioned. Prior to bidding, each player will be given an estimate of the actual value of the item.The estimates may be different between players. The actual value of the item, which is common to all players, will be revealed after the bidding has taken place. < / p > < p > Based on the value estimate, each player will submit a single bid within a given range. All bids are private and submitted at the same time. < / p > < p > The highest bidder will receive the actual value of the item as payoff minus their own bid amount. If the winner 's bid amount is higher than the actual value of the item, the payoff will be zero. In the event of a tie between two or more players, the winner will be chosen at random.Other players will receive nothing. < / p > < / div > < / div > common_value_auction / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} trust_simple / SendBack.html From otree - demo {{block title}}Trust Game: Your Choice {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} < p > You are Participant B.Participant A sent you {{group.sent_amount}} and you received {{tripled_amount}}. < / p > {{formfields}} {{next_button}} {{endblock}} trust_simple / Send.html From otree - demo {{block title}}Trust Game: Your Choice {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} < p > You are Participant A.Now you have {{C.ENDOWMENT}}. < / p > {{formfields}} {{next_button}} {{endblock}} trust_simple / __init__.py From otree - demo from otree.api import * doc = """ Simple trust game """ class C(BaseConstants): NAME_IN_URL = 'trust_simple' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 ENDOWMENT = cu(10) MULTIPLIER = 3 class Subsession(BaseSubsession): pass class Group(BaseGroup): sent_amount = models.CurrencyField( min=cu(0), max=C.ENDOWMENT, doc="""Amount sent by P1""", label="How much do you want to send to participant B?", ) sent_back_amount = models.CurrencyField( doc="""Amount sent back by P2""", label="How much do you want to send back?" ) class Player(BasePlayer): pass # FUNCTIONS def sent_back_amount_choices(group: Group): return currency_range(0, group.sent_amount * C.MULTIPLIER, 1) def set_payoffs(group: Group): p1 = group.get_player_by_id(1) p2 = group.get_player_by_id(2) p1.payoff = C.ENDOWMENT - group.sent_amount + group.sent_back_amount p2.payoff = group.sent_amount * C.MULTIPLIER - group.sent_back_amount # PAGES class Send(Page): form_model = 'group' form_fields = ['sent_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 class WaitForP1(WaitPage): pass class SendBack(Page): form_model = 'group' form_fields = ['sent_back_amount'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 2 @staticmethod def vars_for_template(player: Player): group = player.group return dict(tripled_amount=group.sent_amount * C.MULTIPLIER) class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): pass page_sequence = [Send, WaitForP1, SendBack, ResultsWaitPage, Results] trust_simple / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} {{ if player.id_in_group == 1}} < p > You sent Participant B {{group.sent_amount}}. Participant B returned {{group.sent_back_amount}}. < / p > {{ else}} < p > Participant A sent you {{group.sent_amount}}. You returned {{group.sent_back_amount}}. < / p > {{endif}} < p > Therefore, your total payoff is {{player.payoff}}. < / p > {{include_sibling 'instructions.html'}} {{endblock}} trust_simple / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > This is a trust game with 2 players. < / p > < p > To start, participant A receives {{C.ENDOWMENT}}; participant B receives nothing. Participant A can send some or all of his {{C.ENDOWMENT}} to participant B. Before B receives this amount it will be tripled. Once B receives the tripled amount he can decide to send some or all of it back to A. < / p > < / div > < / div > cournot / __init__.py From otree - demo from otree.api import * doc = """ In Cournot competition, firms simultaneously decide the units of products to manufacture. The unit selling price depends on the total units produced. In this implementation, there are 2 firms competing for 1 period. """ class C(BaseConstants): NAME_IN_URL = 'cournot' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 # Total production capacity of all players TOTAL_CAPACITY = 60 MAX_UNITS_PER_PLAYER = int(TOTAL_CAPACITY / PLAYERS_PER_GROUP) class Subsession(BaseSubsession): pass class Group(BaseGroup): unit_price = models.CurrencyField() total_units = models.IntegerField(doc="""Total units produced by all players""") class Player(BasePlayer): units = models.IntegerField( min=0, max=C.MAX_UNITS_PER_PLAYER, doc="""Quantity of units to produce""", label="How many units will you produce (from 0 to 30)?", ) # FUNCTIONS def set_payoffs(group: Group): players = group.get_players() group.total_units = sum([p.units for p in players]) group.unit_price = C.TOTAL_CAPACITY - group.total_units for p in players: p.payoff = group.unit_price * p.units def other_player(player: Player): return player.get_others_in_group()[0] # PAGES class Introduction(Page): pass class Decide(Page): form_model = 'player' form_fields = ['units'] class ResultsWaitPage(WaitPage): body_text = "Waiting for the other participant to decide." after_all_players_arrive = set_payoffs class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(other_player_units=other_player(player).units) page_sequence = [Introduction, Decide, ResultsWaitPage, Results] cournot / Results.html From otree - demo {{block title}}Results {{endblock}} {{block content}} < p > The results are shown in the following table. < / p > < table class ="table" > < tr > < td > Your firm produced: < / td > < td > {{player.units}} units < / td > < / tr > < tr > < td > The other firm produced: < / td > < td > {{other_player_units}} units < / td > < / tr > < tr > < td > Total production: < / td > < td > {{group.total_units}} units < / td > < / tr > < tr > < td style = "border-top-width:4px" > Unit selling price: < / td > < td style = "border-top-width:4px" > {{C.TOTAL_CAPACITY}} – {{group.total_units}} = {{group.unit_price}} < / td > < / tr > < tr > < td > Your profit: < / td > < td > {{player.payoff}} < / td > < / tr > < / table > {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} cournot / Decide.html From otree - demo {{block title}}Production {{endblock}} {{block content}} {{formfields}} {{next_button}} {{include_sibling 'instructions.html'}} {{endblock}} cournot / instructions.html From otree - demo < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > You have been randomly and anonymously paired with another participant. Each of you will represent a firm.Both firms manufacture the same product. < / p > < p > The two of you decide simultaneously and independently how many units to manufacture. Your choices can be any number from 0 to {{C.MAX_UNITS_PER_PLAYER}}. All produced units will be sold, but the more is produced, the lower the unit selling price will be. < / p > < p > The unit selling price is: < / p > < ul > < strong > Unit selling price = {{C.TOTAL_CAPACITY}} – Units produced by your firm – Units produced by the other firm < / strong > < / ul > < p > Your profit is therefore: < / p > < ul > < strong > Your profit = Unit selling price × Units produced by your firm < / strong > < / ul > < p > For your convenience, these instructions will remain available to you on all subsequent screens of this study. < / p > < / div > < / div > cournot / Introduction.html From otree - demo {{block title}}Introduction {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} supergames_indefinite / __init__.py From otree - more - demos from otree.api import * import random doc = """ Supergames of an indefinitely repeated prisoner's dilemma """ class C(BaseConstants): NAME_IN_URL = 'supergames_indefinite' PLAYERS_PER_GROUP = 2 # this is the number of supergames NUM_ROUNDS = 5 STOPPING_PROBABILITY = 0.2 PAYOFFA = cu(300) PAYOFFB = cu(200) PAYOFFC = cu(100) PAYOFFD = cu(0) # True is cooperate, False is defect PAYOFF_MATRIX = { (True, True): (PAYOFFB, PAYOFFB), (True, False): (PAYOFFD, PAYOFFA), (False, True): (PAYOFFA, PAYOFFD), (False, False): (PAYOFFC, PAYOFFC), } class Subsession(BaseSubsession): pass class Group(BaseGroup): iteration = models.IntegerField(initial=0) finished_sg = models.BooleanField(initial=False) def live_method(player, data): group = player.group my_id = player.id_in_group if group.finished_sg: return {my_id: dict(finished_sg=True)} [game] = Game.filter(group=group, iteration=group.iteration) coop_field = 'coop{}'.format(my_id) if 'coop' in data: coop = data['coop'] if getattr(game, coop_field) is not None: return setattr(game, coop_field, coop) coops = (game.coop1, game.coop2) is_ready = None not in coops if is_ready: p1, p2 = group.get_players() [game.payoff1, game.payoff2] = C.PAYOFF_MATRIX[coops] p1.payoff += game.payoff1 p2.payoff += game.payoff2 game.has_results = True group.iteration += 1 # random stopping rule if random.random() < C.STOPPING_PROBABILITY: group.finished_sg = True return {0: dict(finished_sg=True)} Game.create(group=group, iteration=group.iteration) return { 0: dict(should_wait=False, last_results=to_dict(game), iteration=group.iteration) } i_decided = getattr(game, coop_field) is not None if group.iteration > 0: [prev_game] = Game.filter(group=group, iteration=group.iteration - 1) last_results = to_dict(prev_game) else: last_results = None return { my_id: dict( should_wait=i_decided and not game.has_results, last_results=last_results, iteration=group.iteration, ) } class Game(ExtraModel): group = models.Link(Group) iteration = models.IntegerField() coop1 = models.CurrencyField() coop2 = models.CurrencyField() payoff1 = models.CurrencyField() payoff2 = models.CurrencyField() has_results = models.BooleanField(initial=False) def to_dict(game: Game): return dict(payoffs=[game.payoff1, game.payoff2], coops=[game.coop1, game.coop2]) class Player(BasePlayer): iteration = models.IntegerField(initial=0) class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): # make the first one Game.create(group=group, iteration=group.iteration) class Play(Page): form_model = 'player' live_method = live_method @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) class Results(Page): pass page_sequence = [WaitToStart, Play, Results] supergames_indefinite / Results.html From otree - more - demos {{block title}} Results of supergame {{subsession.round_number}} {{endblock}} {{block content}} < p > The supergame ended due to the random stopping rule. < / p > < p > Your total payoff from the last supergame is {{player.payoff}}. < / p > {{next_button}} {{endblock}} supergames_indefinite / Play.html From otree - more - demos {{block content}} < div id = "wait" style = "display: none" > < p > Waiting for the other player to decide... < / p > < progress > < / progress > < / div > < div id = "decide" style = "display: none" > < div id = "results" style = "display: none" > < p > Here are the results of the last period: < / p > < table class ="table" > < tr > < th > Player < / th > < th > Decision < / th > < / tr > < tr > < td > Me < / td > < td id = "my-decision" > < / td > < / tr > < tr > < td > Other player < / td > < td id = "other-decision" > < / td > < / tr > < / table > < / div > < h5 > Supergame {{subsession.round_number}}, period < span id = "period" > < / span > < / h5 > < label > Please decide: < button type = "button" onclick = "cooperate()" > Cooperate < / button > < button type = "button" onclick = "defect()" > Defect < / button > < / label > < br > < br > {{include_sibling 'instructions.html'}} < / div > < script > let waitDiv = document.getElementById('wait'); let decideDiv = document.getElementById('decide'); let resultsDiv = document.getElementById('results'); let input = document.getElementById('input'); let playerCells = [ document.getElementById('my-decision'), document.getElementById('other-decision') ]; if (js_vars.my_id === 2) playerCells.reverse(); function cooperate() { liveSend({'coop': true}); } function defect() { liveSend({'coop': false}); } function show(ele) { for (let div of[waitDiv, decideDiv]) { div.style.display = (div == = ele) ? 'block': 'none'; } } function showResults(results) { for (let i = 0; i < results.coops.length; i++) { playerCells[i].innerText = results.coops[i] ? 'Cooperate': 'Defect'; } // it 's only hidden in the first period resultsDiv.style.display = 'block'; resultsDiv.style.backgroundColor = 'lightgreen'; setTimeout(function(event) { resultsDiv.style.backgroundColor = ''; }, 1000); } function liveRecv(data) { console.log('liveRecv', JSON.stringify(data)); if (data.finished_sg) { document.getElementById('form').submit(); return; } document.getElementById('period').innerText = data.iteration + 1; let is_waiting = data.should_wait; if (is_waiting) { show(waitDiv); } else { show(decideDiv); } if (data.last_results) { showResults(data.last_results); } } document.addEventListener("DOMContentLoaded", function(event) { liveSend({'type': 'load'}); }); < / script > {{endblock}} supergames_indefinite / instructions.html From otree - more - demos \ < div class ="card bg-light m-3" > < div class ="card-body" > < h3 > Instructions < / h3 > < p > This is an indefinitely repeated prisoner 's dilemma with a stopping probability of {{C.STOPPING_PROBABILITY}}. It will repeat for {{C.NUM_ROUNDS}} supergames. < / p > < p > Here is the payoff matrix: < / p > < table class ='table table-bordered text-center' style = 'width: auto; margin: auto' > < tr > < td > < / td > < th > Other Cooperates < / th > < th > Other Defects < / th > < / tr > < tr > < th > You Cooperate < / th > < td > You: {{C.PAYOFFB}}, Other: {{C.PAYOFFB}} < / td > < td > You: {{C.PAYOFFD}}, Other: {{C.PAYOFFA}} < / td > < / tr > < tr > < th > You Defect < / th > < td > You: {{C.PAYOFFA}}, Other: {{C.PAYOFFD}} < / td > < td > You: {{C.PAYOFFC}}, Other: {{C.PAYOFFC}} < / td > < / tr > < / table > < / div > < / div > continuous_time_slider / chart.html From otree - more - demos < script src = "https://code.highcharts.com/highcharts.js" > < / script > < script src = "https://code.highcharts.com/modules/series-label.js" > < / script > < !-- height is needed to avoid scroll to top: https: // www.highcharts.com / forum / viewtopic.php?t = 12731 --> < div id = "highchart" style = "height: 30em" > < / div > < script > function redrawChart(series) { Highcharts.chart('highchart', { title: { text: 'History' }, yAxis: { title: { text: 'Contributions' }, min: 0, max: js_vars.max_contribution }, xAxis: { title: { text: 'Time (seconds)' }, min: 0 }, plotOptions: { series: { label: { enabled: false }, step: 'left', animation: false }, line: { marker: { enabled: false } } }, series: series, credits: { enabled: false } }); } < / script > continuous_time_slider / __init__.py From otree - more - demos from otree.api import * doc = """ Continuous-time public goods game with slider """ class C(BaseConstants): NAME_IN_URL = 'continuous_slider' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 MAX_CONTRIBUTION = cu(100) class Subsession(BaseSubsession): pass class Group(BaseGroup): start_timestamp = models.IntegerField() class Player(BasePlayer): pass class Adjustment(ExtraModel): group = models.Link(Group) player = models.Link(Player) contribution = models.CurrencyField() seconds = models.IntegerField(doc="Timestamp (seconds since beginning of trading)") class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): import time group.start_timestamp = int(time.time()) for p in group.get_players(): Adjustment.create( player=p, group=group, contribution=C.MAX_CONTRIBUTION / 2, seconds=0, ) # PAGES class MyPage(Page): timeout_seconds = 5 * 60 @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group, max_contribution=C.MAX_CONTRIBUTION) @staticmethod def vars_for_template(player: Player): return dict(max_contribution=int(C.MAX_CONTRIBUTION)) @staticmethod def live_method(player: Player, data): group = player.group import time # print('data is', data) now_seconds = int(time.time() - group.start_timestamp) if 'contribution' in data: contribution = data['contribution'] Adjustment.create( player=player, group=group, contribution=contribution, seconds=now_seconds, ) highcharts_series = [] for p in group.get_players(): history = [[adj.seconds, adj.contribution] for adj in Adjustment.filter(player=p)] # this is optional. it allows the line # to go all the way to the right of the graph last_contribution = history[-1][1] history.append([now_seconds, last_contribution]) series = dict(data=history, type='line', name='Player {}'.format(p.id_in_group)) highcharts_series.append(series) return {0: dict(highcharts_series=highcharts_series)} class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): # to be filled in. # you should calculate some results here. maybe aggregate all the Adjustments, # take their weighted average, etc. # adjustments = Adjustment.filter(group=group) pass class Results(Page): pass page_sequence = [WaitToStart, MyPage, ResultsWaitPage, Results] continuous_time_slider / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > < i > Calculate your results then show them here... < / i > < / p > {{endblock}} continuous_time_slider / MyPage.html From otree - more - demos {{block content}} < p > This is a continuous - time public goods game.You are < b > Player {{player.id_in_group}} < / b >.< / p > < p > Use the slider to adjust your contribution in real time: < / p > < input type = "range" min = "0" max = "{{ max_contribution }}" step = "1" class ="form-range" onchange = "updateContribution(this)" > < br > < br > < p > Seconds since last change: < span id = "secondsSinceChange" > 0 < / span > < / p > {{include_sibling 'chart.html'}} < script > let secondsSinceChange = 0; function updateContribution(slider) { liveSend({'contribution': parseInt(slider.value)}); } function liveRecv(data) { console.log(data) if ('highcharts_series' in data) { redrawChart(data.highcharts_series); secondsSinceChange = 0; } } setInterval(function() { secondsSinceChange + +; document.getElementById('secondsSinceChange').innerHTML = secondsSinceChange; }, 1000); document.addEventListener('DOMContentLoaded', (event) = > { liveSend({}); }); < / script > {{endblock}} monty_hall / Decide2.html From otree - more - demos {{block title}} Final decision {{endblock}} {{block content}} < p > We now reveal that door {{player.door_revealed}} does not contain the prize. Do you want to stay with door {{player.door_first_chosen}} or switch to door {{player.door_not_revealed}}? < / p > < button name = "door_finally_chosen" value = "{{ player.door_first_chosen }}" > Stay with door {{player.door_first_chosen}} < / button > < button name = "door_finally_chosen" value = "{{ player.door_not_revealed }}" > Switch to door {{player.door_not_revealed}} < / button > {{endblock}} monty_hall / __init__.py From otree - more - demos from otree.api import * doc = """ Monty Hall problem """ class C(BaseConstants): NAME_IN_URL = 'monty_hall' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): door_first_chosen = models.IntegerField(choices=[1, 2, 3]) door_revealed = models.IntegerField() door_not_revealed = models.IntegerField() door_finally_chosen = models.IntegerField() door_with_prize = models.IntegerField() is_winner = models.BooleanField() def door_finally_chosen_choices(player: Player): return [player.door_first_chosen, player.door_not_revealed] class Decide1(Page): form_model = 'player' form_fields = ['door_first_chosen'] @staticmethod def before_next_page(player: Player, timeout_happened): import random other_doors = [1, 2, 3] other_doors.remove(player.door_first_chosen) random.shuffle(other_doors) player.door_with_prize = random.choice([1, 2, 3]) other_doors.sort(key=lambda door: door == player.door_with_prize) [player.door_revealed, player.door_not_revealed] = other_doors class Decide2(Page): form_model = 'player' form_fields = ['door_finally_chosen'] @staticmethod def before_next_page(player: Player, timeout_happened): player.is_winner = player.door_finally_chosen == player.door_with_prize class Results(Page): pass page_sequence = [Decide1, Decide2, Results] monty_hall / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > The door with the prize was {{player.door_with_prize}}. {{ if player.is_winner}} You won! {{ else}} Better luck next time. {{endif}} < / p > {{endblock}} monty_hall / Decide1.html From otree - more - demos {{block title}} Choose a door {{endblock}} {{block content}} < p > Behind one of these doors there is a prize.Which door will you pick? < / p > < p > After you make your choice, we will reveal one of the doors that does not contain the prize, and you will have a chance to change your choice. < / p > < button name = "door_first_chosen" value = "1" > Door 1 < / button > < button name = "door_first_chosen" value = "2" > Door 2 < / button > < button name = "door_first_chosen" value = "3" > Door 3 < / button > {{endblock}} go_no_go / Task.html From otree - more - demos {{block title}} {{endblock}} {{block content}} {{ for path in image_paths}} < img class ="img-stimulus" src="{{ static path }}" style="display: none" > < / img > {{endfor}} < div id = "feedback" style = "font-size: 100px" > < / div > < div id = "loading" > Get ready... < / div > < script > let images = document.getElementsByClassName('img-stimulus'); let feedback = document.getElementById('feedback'); let displayed_timestamp; let loading = document.getElementById('loading'); let image_id_global = null; // time before we unhideDiv the first image(give time to get hands ready on keyboard) const INITIAL_DELAY = 2000; // time in between showing showing ✓ or ✗, and showing the next image const IN_BETWEEN_DELAY = 1000; function liveRecv(data) { for (let image of images) { image.style.display = 'none'; } if (data.feedback) { feedback.innerHTML = data.feedback; feedback.style.display = 'block'; } if (data.is_finished) { document.getElementById('form').submit(); } else { image_id_global = data.image_id; setTimeout(() = > loadImage(data.image_id), IN_BETWEEN_DELAY); } } function loadImage(image_id) { feedback.style.display = 'none'; images[image_id].style.display = 'block'; isRefractoryPeriod = false; displayed_timestamp = performance.now(); setTimeout(() = > { liveSend({'image_id': image_id, 'pressed': false}) }, 3000); } let isRefractoryPeriod = false; document.addEventListener("keypress", function(event) { if (event.key === '1') { if (isRefractoryPeriod) return; isRefractoryPeriod = true; liveSend({ 'image_id': image_id_global, 'pressed': true, 'displayed_timestamp': displayed_timestamp, 'answered_timestamp': performance.now() }) } }); document.addEventListener('DOMContentLoaded', function(event) { setTimeout(function() { loading.style.display = 'none'; liveSend({}); }, INITIAL_DELAY); }); < / script > {{endblock}} read_mind_in_eyes / __init__.py From otree - more - demos from otree.api import * doc = """ Reading the Mind in the Eyes Test (Baron-Cohen et al. 2001). See here: http://socialintelligence.labinthewild.org/mite/ """ def read_csv(): import csv f = open(__name__ + '/stimuli.csv', encoding='utf-8-sig') rows = [row for row in csv.DictReader(f)] for row in rows: row['image_path'] = 'read_mind_in_eyes/{}.png'.format(row['image']) return {row['image']: row for row in rows} class C(BaseConstants): NAME_IN_URL = 'read_mind_in_eyes' PLAYERS_PER_GROUP = None IMAGES = read_csv() NUM_ROUNDS = len(IMAGES) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): choice = models.StringField( label="What emotion are the eyes showing?", widget=widgets.RadioSelect ) is_correct = models.BooleanField() def choice_choices(player: Player): trial = get_current_trial(player) return [ ['A', trial['A']], ['B', trial['B']], ['C', trial['C']], ['D', trial['D']], ] def get_current_trial(player: Player): if player.round_number == 1: image_name = 'practice' else: image_name = str(player.round_number - 1) return C.IMAGES[image_name] class Play(Page): form_model = 'player' form_fields = ['choice'] @staticmethod def vars_for_template(player: Player): trial = get_current_trial(player) return dict(image_path=trial['image_path'], is_practice=player.round_number == 1) @staticmethod def before_next_page(player: Player, timeout_happened): trial = get_current_trial(player) player.is_correct = player.choice == trial['solution'] class PracticeFeedback(Page): @staticmethod def is_displayed(player: Player): return player.round_number == 1 class Results(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): participant = player.participant score = sum(p.is_correct for p in player.in_rounds(2, C.NUM_ROUNDS)) participant.read_mind_in_eyes_score = score return dict(score=score) page_sequence = [Play, PracticeFeedback, Results] read_mind_in_eyes / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > You answered {{score}} out of 36 images correctly. < / p > {{endblock}} read_mind_in_eyes / PracticeFeedback.html From otree - more - demos {{block title}} Practice feedback {{endblock}} {{block content}} {{ if player.is_correct}} < p > You got the practice question correct! < / p > {{ else}} < p > The correct answer was actually 'panicked'. < / p > {{endif}} < p > You will receive no feedback for the remaining questions.< / p > {{next_button}} {{endblock}} read_mind_in_eyes / Play.html From otree - more - demos {{block title}} Face {{subsession.round_number}} of {{C.NUM_ROUNDS}} {{ if is_practice}}(practice) {{endif}} {{endblock}} {{block content}} < img src = "{{ static image_path }}" style = "padding-bottom: 100px" > {{formfields}} {{next_button}} {{ if is_practice}} {{include_sibling 'instructions.html'}} {{endif}} {{endblock}} read_mind_in_eyes / instructions.html From otree - more - demos < h3 > Instructions < / h3 > < p > This test will investigate your ability to read emotion from the eyes.You will be shown a pair of eyes with four emotion labels around it.You are to select which one of the four emotion words best describes the emotion that the eyes are showing.Please provide one best guess for each item. < / p > stroop / Introduction.html From otree - more - demos {{block title}} Instructions {{endblock}} {{block content}} This is a stroop test. < p > When you see a word, quickly enter the color of the word(not the word itself) < / p > < p > With your keyboard, press the key representing the first letter of the color. < / p > < table class ="table" > < tr > < th > Color < / th > < th > Letter < / th > < / tr > {{ for entry in C.COLOR_KEYS}} < tr > < td > {{entry .1}} < / td > < td > {{entry .0}} < / td > < / tr > {{endfor}} < / table > < p > When you are ready to start, press the button below. < / p > < button class ="btn btn-primary" > Start < / button > {{endblock}} randomize_stimuli / __init__.py From otree - more - demos import random from otree.api import * from . import rand_functions doc = """ This app is a demonstration of different ordering of stimuli, such as multiple blocks, randomization between blocks, randomization within blocks, and alternating blocks. """ class C(BaseConstants): NAME_IN_URL = 'randomize_stimuli' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): num_completed = models.IntegerField(initial=0) ordering_description = models.StringField() num_trials = models.IntegerField() num_page_loads = models.IntegerField( initial=0, doc="""If more than 1, indicates that the user reloaded the page. This complicates the interpretation of the timestamps.""", ) class Trial(ExtraModel): player = models.Link(Player) optionA = models.StringField() optionB = models.StringField() optionC = models.StringField() choice = models.StringField() block = models.StringField() is_intro = models.BooleanField(initial=False) def to_dict(trial: Trial): return dict( optionA=trial.optionA, optionB=trial.optionB, optionC=trial.optionC, is_intro=trial.is_intro, block=trial.block, id=trial.id, ) def creating_session(subsession: Subsession): for player in subsession.get_players(): ordering_function = random.choice( [ rand_functions.multiple_blocks, rand_functions.randomize_blocks, rand_functions.randomize_within_blocks, rand_functions.randomize_merged, rand_functions.alternate_blocks, ] ) # see the other python file in this folder. # you can substitute one of the other randomization functions, # like randomize_within_blocks, etc. trials = ordering_function() player.num_trials = len(trials) for trial in ordering_function(): # "**" unpacks a dict Trial.create(**trial, player=player) # you can delete this; it's just for the demo. player.ordering_description = ordering_function.__doc__ def is_finished(player: Player): return player.num_completed >= player.num_trials def get_current_trial(player: Player): return Trial.filter(player=player, choice=None)[0] # PAGES class Stimuli(Page): @staticmethod def vars_for_template(player: Player): player.num_page_loads += 1 @staticmethod def live_method(player: Player, data): my_id = player.id_in_group if 'response' in data: if is_finished(player): return trial = get_current_trial(player) # prevent double clicks if data['trialId'] != trial.id: return trial.choice = data['response'] player.num_completed += 1 if is_finished(player): return {my_id: dict(is_finished=True)} trial = get_current_trial(player) payload = dict(stimulus=to_dict(trial)) return {my_id: payload} class Intro(Page): @staticmethod def vars_for_template(player: Player): return dict(doc=doc) class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(trials=Trial.filter(player=player)) page_sequence = [Intro, Stimuli, Results] randomize_stimuli / Results.html From otree - more - demos {{block title}} Your data {{endblock}} {{block content}} < table class ="table" > {{ for trial in trials}} < tr > < td > {{trial}} < / td > < / tr > {{endfor}} < / table > < p > You can export this data to a spreadsheet using < code > custom_export < / code >, or do some calculation based on the responses in < code > before_next_page < / code >. < / p > {{endblock}} randomize_stimuli / Intro.html From otree - more - demos {{block title}} Intro {{endblock}} {{block content}} < p > {{doc}} < / p > < p > For this example, this session has 2 types of questions. The first type is about foods. The second type is about drinks. < / p > < p > The ordering algorithm for this participant has been chosen randomly: < / p > < pre > {{player.ordering_description}} < / pre > < p > When you are ready to start, press the button below. < / p > < button class ="btn btn-primary" > Start < / button > {{endblock}} randomize_stimuli / Stimuli.html From otree - more - demos {{block title}} {{endblock}} {{block content}} < div id = "stimulus-foods" class ="panel" style="display: none" > < p > What food do you prefer? < / p > < button type = "button" onclick = "sendResponse('optionA')" id = "foods-optionA" > < / button > < button type = "button" onclick = "sendResponse('optionB')" id = "foods-optionB" > < / button > < / div > < div id = "stimulus-drinks" class ="panel" style="display: none" > < p > What drink do you prefer? < / p > < button type = "button" onclick = "sendResponse('optionA')" id = "drinks-optionA" > < / button > < button type = "button" onclick = "sendResponse('optionB')" id = "drinks-optionB" > < / button > < button type = "button" onclick = "sendResponse('optionC')" id = "drinks-optionC" > < / button > < / div > < div id = "foods_intro" class ="panel" style="display: none" > < p > Now you will be shown a series of questions about your favorite foods.For each pair, indicate your favorite food. < / p > < button type = "button" onclick = "sendContinue()" > continue < / button > < / div > < div id = "drinks_intro" class ="panel" style="display: none" > < p > Now you will be shown a series of questions about your favorite drinks.For choice of 3 drinks, indicate your favorite drink. < / p > < button type = "button" onclick = "sendContinue()" > continue < / button > < / div > < script > let buttonrow = document.getElementById('buttonrow'); let trialId = null; function unhideDiv(div_id) { // note, 'block' has nothing to do with experimental blocks. // this is just CSS lingo: https: // www.w3schools.com / cssref / pr_class_display.asp document.getElementById(div_id).style.display = 'block'; } function liveRecv(data) { console.log(data) if (data.is_finished) { document.getElementById('form').submit(); } else { for (let div of document.getElementsByClassName('panel')) { div.style.display = 'none'; } let stimulus = data.stimulus; let blockName = stimulus.block trialId = stimulus.id; if (stimulus.is_intro) { let divId = blockName + '_intro'; unhideDiv(divId); } else { let divId = 'stimulus-' + blockName; unhideDiv(divId); for (let key of Object.keys(stimulus)) { if (key.startsWith('option')) { let buttonId = blockName + '-' + key; document.getElementById(buttonId).innerText = stimulus[key] } } } } } function sendResponse(value) { liveSend({'response': value, 'trialId': trialId}); } function sendContinue() { liveSend({'response': 'continue', 'trialId': trialId}); } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > {{endblock}} scheduling_part0 / __init__.py From otree - more - demos from datetime import datetime, timedelta from otree.api import * from scheduling_part1 import C as GroupingAppConstants doc = """ Scheduling players to start at a certain time (part 0: booking app) """ def round_up_minutes(dt: datetime, minutes_step: int): discard = timedelta(minutes=dt.minute % minutes_step, seconds=dt.second) dt -= discard dt += timedelta(minutes=minutes_step) return dt class C(BaseConstants): NAME_IN_URL = 'scheduling_part0' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 BATCHES_PER_HOUR = 6 NUM_BATCHES = 10 FIRST_BATCH_DELAY_MINUTES = 0 PLAYERS_PER_BATCH = GroupingAppConstants.PLAYERS_PER_GROUP class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass class Batch(ExtraModel): subsession = models.Link(Subsession) time = models.FloatField() number = models.IntegerField() class Slot(ExtraModel): subsession = models.Link(Subsession) batch = models.Link(Batch) player = models.Link(Player) def creating_session(subsession: Subsession): minutes_step = int(60 / C.BATCHES_PER_HOUR) now = datetime.now() first_slot_time = round_up_minutes( now + timedelta(minutes=C.FIRST_BATCH_DELAY_MINUTES), minutes_step ) for i in range(C.NUM_BATCHES): dt = first_slot_time + timedelta(minutes=minutes_step * i) batch = Batch.create(subsession=subsession, time=dt.timestamp(), number=i) for j in range(C.PLAYERS_PER_BATCH): Slot.create(subsession=subsession, batch=batch) def live_booking(player: Player, data): """ It doesn't matter whether we use group or subsession here, because players_per_group = None. """ subsession = player.subsession msg_type = data['type'] if msg_type == 'book' or msg_type == 'cancel': for slot in Slot.filter(player=player): slot.player = None if msg_type == 'book': [batch] = Batch.filter(number=data['batch_number'], subsession=subsession) free_slots = Slot.filter(batch=batch, player=None) if free_slots: free_slots[0].player = player batches_data = [] batches = Batch.filter(subsession=subsession) for b in batches: slots = Slot.filter(batch=b, subsession=subsession) num_free_slots = 0 player_ids = [] for s in slots: if s.player: player_ids.append(s.player.id_in_group) else: num_free_slots += 1 batches_data.append( dict( time=b.time, number=b.number, player_ids=player_ids, num_free_slots=num_free_slots, ) ) return {0: dict(batches=batches_data)} class Booking(Page): live_method = live_booking @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def error_message(player: Player, values): if not Slot.filter(player=player): return "You have not made a booking yet" @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant [slot] = Slot.filter(player=player) participant.booking_time = slot.batch.time class ResultsWaitPage(WaitPage): pass page_sequence = [Booking] scheduling_part0 / Booking.html From otree - more - demos {{block title}} Booking {{endblock}} {{block content}} < p > You are about to take part in a game that requires several people online at the same time. To coordinate, please book one of the below time slots. < / p > < table class ="table" > < thead > < th > Slot < / th > < th > Bookings < / th > < th > Openings < / th > < th > Note < / th > < th > < / th > < th > < / th > < th > < / th > < / thead > < tbody id = "batches" > < / tbody > < / table > < p > When it 's time for your scheduled game, press the button below. < / p > < button id = "next-button" class ="btn btn-primary" > Move to waiting page < / button > < !-- luxon library for date formatting --> < script src = "https://cdnjs.cloudflare.com/ajax/libs/luxon/2.0.2/luxon.min.js" integrity = "sha512-frUCURIeB0OKMPgmDEwT3rC4NH2a4gn06N3Iw6T1z0WfrQZd7gNfJFbHrNsZP38PVXOp6nUiFtBqVvmCj+ARhw==" crossorigin = "anonymous" referrerpolicy = "no-referrer" > < / script > < script > const DateTime = luxon.DateTime; let table = document.getElementById('batches'); function liveRecv(data) { if (data.batches) { table.innerHTML = ''; for (let batch of data.batches) { let has_free_slots = batch.num_free_slots > 0 let player_ids = batch.player_ids; let numPlayers = player_ids.length; let bookedByMe = player_ids.includes(js_vars.my_id) let isInFuture = batch.time * 1000 > Date.now(); if (bookedByMe | | (has_free_slots & & isInFuture)) { let bookBtn, cancelBtn, bookedIndicator; if (bookedByMe) { bookBtn = ''; cancelBtn = ` < button type = "button" onclick = "cancelBooking(this)" > Cancel < / button > `; bookedIndicator = "You are booked"; document.getElementById('next-button').style.display = ''; } else { bookBtn = has_free_slots ? ` < button type = "button" onclick = "makeBooking(this)" > Book < / button > `: ''; cancelBtn = ''; bookedIndicator = ''; } let tr = ` < tr data - batch = "${batch.number}" > < td data - time = "${batch.time}" class ="batch-time" > < / td > < td >${numPlayers} < / td > < td >${batch.num_free_slots} < / td > < td >${bookedIndicator} < / td > < td >${bookBtn} < / td > < td >${cancelBtn} < / td > < / tr > `; table.insertAdjacentHTML('beforeend', tr); } } formatAllTimestamps(); } } function makeBooking(btn) { let batch_number = parseInt(btn.parentNode.parentNode.dataset.batch); liveSend({'type': 'book', 'batch_number': batch_number}); } function cancelBooking(btn) { let batch_number = parseInt(btn.parentNode.parentNode.dataset.batch); liveSend({'type': 'cancel'}); } document.addEventListener("DOMContentLoaded", function(event) { liveSend({'type': 'load'}); }); function formatAllTimestamps() { for (let ele of document.getElementsByClassName('batch-time')) { let dt = DateTime.fromSeconds(parseInt(ele.dataset.time)); let timeAbs = dt.toLocaleString(DateTime.TIME_SIMPLE, {locale: 'en'}); let timeRel = dt.toRelative({locale: 'en'}); ele.innerHTML = `${timeAbs}(${timeRel})`; } } setInterval(formatAllTimestamps, 5 * 1000); < / script > {{endblock}} rockpaperscissors / history.html From otree - more - demos < br > < br > < h3 > History < / h3 > < table class ="table" > < tr > < th > Round number < / th > < th > Your move < / th > < th > Partner 's move < th > Result < / th > < / tr > {{ for past_player in past_players}} < tr > < td > {{past_player.round_number}} < / td > < td > {{past_player.hand}} < / td > < td > {{past_player.opponent_hand}} < / td > < td > {{past_player.result}} < / td > < / tr > {{endfor}} < / table > rockpaperscissors / __init__.py From otree - more - demos from otree.api import * class C(BaseConstants): NAME_IN_URL = 'rockpaperscissors' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 10 CHOICES = ['Rock', 'Paper', 'Scissors'] class Subsession(BaseSubsession): pass class Group(BaseGroup): is_draw = models.BooleanField() class Player(BasePlayer): hand = models.StringField(choices=C.CHOICES) opponent_hand = models.StringField() result = models.StringField() def set_winner(player: Player): [opponent] = player.get_others_in_group() if player.hand == opponent.hand: player.result = 'Draw' elif player.hand + opponent.hand in 'ScissorsPaperRockScissors': player.result = 'Win' else: player.result = 'Loss' player.opponent_hand = opponent.hand class Shoot(Page): form_model = 'player' form_fields = ['hand'] @staticmethod def vars_for_template(player: Player): return dict(past_players=player.in_previous_rounds()) class WaitForOther(WaitPage): @staticmethod def after_all_players_arrive(group: Group): for p in group.get_players(): set_winner(p) class FinalResults(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): return dict(past_players=player.in_all_rounds()) page_sequence = [Shoot, WaitForOther, FinalResults] rockpaperscissors / FinalResults.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} {{include_sibling 'history.html'}} {{endblock}} rockpaperscissors / Shoot.html From otree - more - demos {{block title}} Round {{subsession.round_number}}, Shoot! {{endblock}} {{block content}} {{ for choice in C.CHOICES}} < button name = "hand" value = "{{choice}}" > {{choice}} < / button > {{endfor}} {{ if subsession.round_number > 1}} {{include_sibling 'history.html'}} {{endif}} {{endblock}} double_auction / chart.html From otree - more - demos < script src = "https://code.highcharts.com/highcharts.js" > < / script > < script src = "https://code.highcharts.com/modules/series-label.js" > < / script > < div id = "highchart" > < / div > < script > function redrawChart(series) { Highcharts.chart('highchart', { title: { text: 'Trade history' }, yAxis: { title: { text: 'Price' } }, xAxis: { title: { text: 'Time (seconds)' }, min: 0 }, plotOptions: { series: { label: { enabled: false }, } }, series: [{ data: series, type: 'scatter' }], credits: { enabled: false } }); } < / script > double_auction / __init__.py From otree - more - demos from otree.api import * import time import random doc = "Double auction market" class C(BaseConstants): NAME_IN_URL = 'double_auction' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 ITEMS_PER_SELLER = 3 VALUATION_MIN = cu(50) VALUATION_MAX = cu(110) PRODUCTION_COSTS_MIN = cu(10) PRODUCTION_COSTS_MAX = cu(80) class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): players = subsession.get_players() for p in players: # this means if the player's ID is not a multiple of 2, they are a buyer. # for more buyers, change the 2 to 3 p.is_buyer = p.id_in_group % 2 > 0 if p.is_buyer: p.num_items = 0 p.break_even_point = random.randint(C.VALUATION_MIN, C.VALUATION_MAX) p.current_offer = 0 else: p.num_items = C.ITEMS_PER_SELLER p.break_even_point = random.randint( C.PRODUCTION_COSTS_MIN, C.PRODUCTION_COSTS_MAX ) p.current_offer = C.VALUATION_MAX + 1 class Group(BaseGroup): start_timestamp = models.IntegerField() class Player(BasePlayer): is_buyer = models.BooleanField() current_offer = models.CurrencyField() break_even_point = models.CurrencyField() num_items = models.IntegerField() class Transaction(ExtraModel): group = models.Link(Group) buyer = models.Link(Player) seller = models.Link(Player) price = models.CurrencyField() seconds = models.IntegerField(doc="Timestamp (seconds since beginning of trading)") def find_match(buyers, sellers): for buyer in buyers: for seller in sellers: if seller.num_items > 0 and seller.current_offer <= buyer.current_offer: # return as soon as we find a match (the rest of the loop will be skipped) return [buyer, seller] def live_method(player: Player, data): group = player.group players = group.get_players() buyers = [p for p in players if p.is_buyer] sellers = [p for p in players if not p.is_buyer] news = None if data: offer = int(data['offer']) player.current_offer = offer if player.is_buyer: match = find_match(buyers=[player], sellers=sellers) else: match = find_match(buyers=buyers, sellers=[player]) if match: [buyer, seller] = match price = buyer.current_offer Transaction.create( group=group, buyer=buyer, seller=seller, price=price, seconds=int(time.time() - group.start_timestamp), ) buyer.num_items += 1 seller.num_items -= 1 buyer.payoff += buyer.break_even_point - price seller.payoff += price - seller.break_even_point buyer.current_offer = 0 seller.current_offer = C.VALUATION_MAX + 1 news = dict(buyer=buyer.id_in_group, seller=seller.id_in_group, price=price) bids = sorted([p.current_offer for p in buyers if p.current_offer > 0], reverse=True) asks = sorted([p.current_offer for p in sellers if p.current_offer <= C.VALUATION_MAX]) highcharts_series = [[tx.seconds, tx.price] for tx in Transaction.filter(group=group)] return { p.id_in_group: dict( num_items=p.num_items, current_offer=p.current_offer, payoff=p.payoff, bids=bids, asks=asks, highcharts_series=highcharts_series, news=news, ) for p in players } # PAGES class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): group.start_timestamp = int(time.time()) class Trading(Page): live_method = live_method @staticmethod def js_vars(player: Player): return dict(id_in_group=player.id_in_group, is_buyer=player.is_buyer) @staticmethod def get_timeout_seconds(player: Player): import time group = player.group return (group.start_timestamp + 2 * 60) - time.time() class ResultsWaitPage(WaitPage): pass class Results(Page): pass page_sequence = [WaitToStart, Trading, ResultsWaitPage, Results] double_auction / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < table class ="table" > < tr > < th > Items in your possession < / th > < td > {{player.num_items}} < / td > < / tr > < tr > < th > Payoff < / th > < td > {{player.payoff}} < / td > < / tr > < / table > {{endblock}} double_auction / Trading.html From otree - more - demos {{block title}} Trade {{endblock}} {{block content}} < p id = "news" style = "color: green" > < / p > < table class ="table" > < tr > < td > Your role < / td > < th > {{ if player.is_buyer}}buyer {{ else}}seller {{endif}} < / th > < / tr > < tr > < td > Your break -even point {{ if player.is_buyer}} (you should buy for less than) {{ else}} (you should sell for more than) {{endif}} < / td > < th > {{player.break_even_point}} < / th > < / tr > < tr > < td > Items in your possession < / td > < th id = "num_items" > < / th > < / tr > < tr > < td > Your current offer < / td > < th id = "current_offer" > < / th > < / tr > < tr > < td > Profits < / td > < th id = "payoff" > < / th > < / tr > < / table > < input type = "number" id = "my_offer" > < button type = "button" onclick = "sendOffer()" id = "btn-offer" > Make offer < / button > < br > < br > < div class ="container" > < div class ="row" > < div class ="col-sm" > < h4 > Bids < / h4 > < table id = "bids_table" > < / table > < / div > < div class ="col-sm" > < h4 > Asks < / h4 > < table id = "asks_table" > < / table > < / div > < / div > < / div > < br > < br > {{include 'double_auction/chart.html'}} < script > let bids_table = document.getElementById('bids_table'); let asks_table = document.getElementById('asks_table'); let my_id = js_vars.id_in_group; let news_div = document.getElementById('news'); let is_buyer = js_vars.is_buyer; let btnOffer = document.getElementById('btn-offer'); let my_offer = document.getElementById('my_offer'); function showNews(msg) { news_div.innerText = msg; setTimeout(function() { news_div.innerText = '' }, 10000) } function cu(amount) { return `${amount} points `; } function liveRecv(data) { console.log(data) // javascript destructuring assignment let {bids, asks, highcharts_series, num_items, current_offer, payoff, news} = data; if (news) { let {buyer, seller, price} = news; if (buyer == = my_id) { showNews(`You bought from player ${seller} for ${cu(price)}`); } else if (seller == = my_id) { showNews(`You sold to player ${buyer} for ${cu(price)}`); } } document.getElementById('num_items').innerText = num_items; document.getElementById('current_offer').innerText = cu(current_offer); document.getElementById('payoff').innerText = cu(payoff); if (!is_buyer & & num_items == = 0) { btnOffer.disabled = true; } bids_table.innerHTML = bids.map(e= > ` < tr > < td >${cu(e)} < / td > < / tr > `).join(''); asks_table.innerHTML = asks.map(e= > ` < tr > < td >${cu(e)} < / td > < / tr > `).join(''); redrawChart(highcharts_series); } function sendOffer() { liveSend({'offer': my_offer.value}) } my_offer.addEventListener("keydown", function(event) { if (event.key === "Enter") { sendOffer(); } }); document.addEventListener('DOMContentLoaded', (event) = > { liveSend({}); }); < / script > {{endblock}} live_coordination / __init__.py From otree - more - demos from otree.api import * doc = """ Live coordination (voting with chat/negotiation) """ class C(BaseConstants): NAME_IN_URL = 'live_coordination' PLAYERS_PER_GROUP = 6 NUM_ROUNDS = 1 MAX_POINTS = 5 CHOICES = [0, 1, 2, 3, 4, 5] MAJORITY = int(PLAYERS_PER_GROUP / 2) + 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): contribution = models.CurrencyField() class Player(BasePlayer): vote = models.IntegerField() # PAGES class Coordinate(Page): @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def live_method(player: Player, data): group = player.group if 'vote' in data: try: vote = int(data['vote']) except Exception: print('Invalid message received', data) return if not vote in C.CHOICES: print('Invalid message received', data) return player.vote = vote players = group.get_players() tallies = {vote: 0 for vote in C.CHOICES} votes = [] for p in players: vote = p.field_maybe_none('vote') if vote is not None: tallies[vote] += 1 if tallies[vote] >= C.MAJORITY: group.contribution = vote return {0: dict(finished=True)} votes.append([p.id_in_group, vote]) # if you don't want to show who voted, use 'tallies' instead of 'votes'. return {0: dict(votes=votes, tallies=tallies)} @staticmethod def is_displayed(player: Player): """Skip this page if a deal has already been made""" group = player.group contribution = group.field_maybe_none('contribution') return contribution is None @staticmethod def error_message(player: Player, values): group = player.group # anti-cheating measure if group.field_maybe_none('contribution') is None: return "Not done with this page" class Results(Page): pass page_sequence = [Coordinate, Results] live_coordination / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} Your team opted for a contribution of {{group.contribution}}. {{endblock}} live_coordination / Coordinate.html From otree - more - demos {{block title}} Voting {{endblock}} {{block content}} < p > How many points should your team contribute to the shared pool? Once a majority agree on an amount, you can proceed to the next page. < / p > < table class ="table table-striped" > < tr > < th > Choice < / th > < th > My vote < / th > < th > Voters < / th > < / tr > {{ for choice in C.CHOICES}} < tr > < th > {{choice}} points < / th > < td > < input type = "radio" name = "my-vote" value = "{{ choice }}" id = "radio-{{ choice }}" onclick = "vote(this)" > < / td > < td > < ul id = "votes-for-{{ choice }}" class ="clear-on-redraw" > < / ul > < / td > < / tr > {{endfor}} < / table > < h4 > Undecided players < / h4 > < ul id = "undecided" class ="clear-on-redraw" > < / ul > < h4 > Chat with teammates {{chat}} < script > let redrawableNodes = document.getElementsByClassName('clear-on-redraw'); function vote(btn) { liveSend({vote: parseInt(btn.value)}); } function liveRecv(data) { if ('finished' in data) { document.getElementById('form').submit(); } if ('votes' in data) { for (let ele of redrawableNodes) { ele.innerHTML = ''; } for (let[id_in_group, vote] of data.votes) { let playerName = `Participant ${id_in_group}`; let isMe = id_in_group == = js_vars.my_id; let isNull = vote == = null; if (isMe) { playerName += ' (me)'; if (!isNull) { document.getElementById(`radio-${vote}`).checked = true; } } let bulletListId = isNull ? 'undecided': `votes - for -${vote}`; let bulletList = document.getElementById(bulletListId); bulletList.innerHTML += ` < li > ${playerName} < / li > `; } } } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > {{endblock}} scheduling_part1 / __init__.py From otree - more - demos import time from otree.api import * doc = """ Scheduling players to start at a certain time (part 1: grouping app) """ class C(BaseConstants): NAME_IN_URL = 'scheduling' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 GRACE_MINUTES = 5 class Subsession(BaseSubsession): pass def group_by_arrival_time_method(subsession: Subsession, waiting_players): # the eligible players are the ones whose booking time has come. # they stay eligible for a few minutes. # players can also get grouped even if this is not their official time, # but they get lower priority than the grouped players. now = time.time() # filter out players who booked a later slot eligible_players = [p for p in waiting_players if p.participant.booking_time < now] expected_players = [] # stand-by players are the ones whose time already passed, but they did not get grouped, # either because they arrived late and missed it, or not enough other people showed up standby_players = [] for p in eligible_players: if p.participant.booking_time > now - C.GRACE_MINUTES * 60: expected_players.append(p) else: standby_players.append(p) prioritized_players = expected_players + standby_players if len(prioritized_players) >= C.PLAYERS_PER_GROUP: return prioritized_players[: C.PLAYERS_PER_GROUP] class Group(BaseGroup): pass class Player(BasePlayer): pass class GroupingWaitPage(WaitPage): group_by_arrival_time = True class MyPage(Page): pass page_sequence = [GroupingWaitPage, MyPage] scheduling_part1 / Results.html From otree - more - demos {{block title}} Page title {{endblock}} {{block content}} {{next_button}} {{endblock}} scheduling_part1 / MyPage.html From otree - more - demos {{block title}} Game {{endblock}} {{block content}} < p > Your game goes here... < / p > {{endblock}} crazy_eights / __init__.py From otree - more - demos from otree.api import * import random import json doc = """ Card game (crazy eights) """ SUITS = 'SHDC' NUMBERS = 'A23456789TJQK' DECK = tuple([number + suit for number in NUMBERS for suit in SUITS]) NUMBERS_TO_CODEPOINTS = dict(zip(NUMBERS, '123456789ABDE')) SUITS_TO_CODEPOINTS = dict(zip('SHDC', 'ABCD')) class C(BaseConstants): NAME_IN_URL = 'crazy_eights' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): current_card = models.StringField(initial='') stock = models.LongStringField() whose_turn = models.IntegerField(initial=1) class Player(BasePlayer): hand = models.LongStringField() is_winner = models.BooleanField(initial=False) class WaitToPlay(WaitPage): @staticmethod def after_all_players_arrive(group: Group): players = group.get_players() deck = list(DECK) random.shuffle(deck) for p in players: p.hand = json.dumps(deck[:8]) deck = deck[8:] group.stock = json.dumps(deck) def is_legal(card, current_card): return ( current_card == '' or card[0] == current_card[0] or card[1] == current_card[1] or card[0] == '8' ) def increment_turn(group: Group): group.whose_turn += 1 if group.whose_turn > C.PLAYERS_PER_GROUP: group.whose_turn = 1 def live_method(player: Player, data): group = player.group my_id = player.id_in_group hand = json.loads(player.hand) current_card = group.current_card msg_type = data['type'] if msg_type != 'load': if my_id != group.whose_turn: return {my_id: dict(type='error', msg='Not your turn')} if msg_type == 'move': card = data['move'] if card in hand and is_legal(card, current_card): hand.remove(card) group.current_card = card if not hand: player.is_winner = True return {0: dict(finished=True)} player.hand = json.dumps(hand) increment_turn(group) if msg_type == 'stock': stock = json.loads(group.stock) if stock: card = stock.pop() group.stock = json.dumps(stock) player.hand = json.dumps(hand + [card]) else: # if the stock is empty, just pass to the next player increment_turn(group) players = group.get_players() card_counts = [[p.id_in_group, len(json.loads(p.hand))] for p in players] return { p.id_in_group: dict( whose_turn=group.whose_turn, hand=json.loads(p.hand), starter=group.current_card, card_counts=card_counts, ) for p in players } class Play(Page): @staticmethod def js_vars(player: Player): return dict( NUMBERS_TO_CODEPOINTS=NUMBERS_TO_CODEPOINTS, SUITS_TO_CODEPOINTS=SUITS_TO_CODEPOINTS, my_id=player.id_in_group, ) live_method = live_method class Results(Page): pass page_sequence = [WaitToPlay, Play, Results] crazy_eights / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} {{ if player.is_winner}} You won! {{ else}} Another player won: ( {{endif}} {{endblock}} go_no_go / __init__.py From otree-more-demos from otree.api import * doc = """ Go/No-go """ class C(BaseConstants): NAME_IN_URL = 'go_no_go' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 RED_IMAGES = [0, 4, 8, 17] NUM_IMAGES = 10 # actually there are 20 images but we just show 10 for brevity class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): participant = p.participant image_ids = generate_ordering() for stim in image_ids: is_red = stim in C.RED_IMAGES Trial.create(player=p, image_id=stim, is_red=is_red) participant.reaction_times = [] class Group(BaseGroup): pass class Player(BasePlayer): num_completed = models.IntegerField(initial=0) num_errors = models.IntegerField(initial=0) avg_reaction_ms = models.FloatField() def get_current_trial(player: Player): return Trial.filter(player=player, is_error=None)[0] def is_finished(player: Player): return player.num_completed == C.NUM_IMAGES class Trial(ExtraModel): player = models.Link(Player) reaction_ms = models.IntegerField() image_id = models.IntegerField() is_red = models.BooleanField() is_error = models.BooleanField() pressed = models.BooleanField() def generate_ordering(): import random numbers = list(range(C.NUM_IMAGES)) random.shuffle(numbers) return numbers # PAGES class Introduction(Page): pass class Task(Page): @staticmethod def live_method(player: Player, data): participant = player.participant if 'pressed' in data: trial = get_current_trial(player) # this is necessary because the timeout will cause duplicates to be sent if data['image_id'] != trial.image_id: return trial.is_error = trial.is_red == data['pressed'] if trial.is_error: feedback = '✗' player.num_errors += 1 else: feedback = '✓' if not trial.is_red: trial.reaction_ms = data['answered_timestamp'] - data['displayed_timestamp'] participant.reaction_times.append(trial.reaction_ms) player.num_completed += 1 else: feedback = '' if is_finished(player): return {player.id_in_group: dict(is_finished=True)} trial = get_current_trial(player) return { player.id_in_group: dict(image_id=trial.image_id, feedback=feedback, trialId=trial.id) } @staticmethod def vars_for_template(player: Player): image_paths = [ 'go_no_go/{}.png'.format(image_id) for image_id in range(C.NUM_IMAGES) ] return dict(image_paths=image_paths) @staticmethod def before_next_page(player: Player, timeout_happened): participant = player.participant import statistics # if the participant never pressed, this list will be empty if participant.reaction_times: avg_reaction = statistics.mean(participant.reaction_times) player.avg_reaction_ms = int(avg_reaction) class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(avg_reaction_ms=player.field_maybe_none('avg_reaction_ms')) page_sequence = [Introduction, Task, Results] crazy_eights / Play.html From otree - more - demos {{block content}} < style > .card - icon { font - size: 7em; } .red - suit { color: red; } .black - suit { color: black; } .card - button { border: none; } < / style > < div id = "status" > < / div > < h5 > Current card < / h5 > < div id = "starter" > < / div > < h5 > Your hand < / h5 > < div id = "hand" > < / div > < br > < p > Click the deck to take a new card < / p > < div > < button type = "button" class ="card-button card-icon" onclick="sendStock()" > 🂠 < / button > < / div > < br > < h5 > Card counts < / h5 > < table class ="table" id="scores" > < / table > < h5 > Instructions < / h5 > < ul > < li > Click a card to play it.It must match the suit or number of the last chosen card < / li > < li > If you have no playable card, take a new one from the deck.< / li > < / ul > < script > let handDiv = document.getElementById('hand'); let statusDiv = document.getElementById('status'); let starterDiv = document.getElementById('starter'); let scoresTable = document.getElementById('scores'); function makeIcon(card) { if (card === '') { return '(blank; please place any card)' } let number = card[0]; let suit = card[1]; let number_char = js_vars.NUMBERS_TO_CODEPOINTS[number]; let suit_char = js_vars.SUITS_TO_CODEPOINTS[suit]; let color = 'HD'.includes(suit) ? 'red': 'black'; // there are unicode characters for each playing card return ` < span class ="card-icon ${color}-suit" > & # x1F0${suit_char}${number_char}`; } function makeButton(card) { let icon = makeIcon(card); return ` < button type = "button" value = "${card}" onclick = "sendCard(this)" class ="card-button pick" > ${icon} < / button > `; } function layoutHand(hand) { handDiv.innerHTML = hand.map(e= > makeButton(e)).join(''); } function sendCard(btn) { let card = btn.value; liveSend({'type': 'move', 'move': card}); } function sendPass() { liveSend({'type': 'pass'}); } function sendStock() { liveSend({'type': 'stock'}); } function enableButtons(enabled) { for (let ele of document.getElementsByClassName('card-button')) { ele.disabled = enabled ? '': 'disabled'; console.log(ele.disabled); } } function liveRecv(data) { console.log(data); if (data.finished) { document.getElementById('form').submit(); } if (data.error) { window.alert(data.msg); return; } let isMyTurn = data.whose_turn == = js_vars.my_id; console.log('ismyturn', isMyTurn); layoutHand(data.hand); enableButtons(isMyTurn); starterDiv.innerHTML = makeIcon(data.starter); let status; if (isMyTurn) { status = ` < span style = "color: green" > It 's your turn`; } else { status = `It 's player ${data.whose_turn}' s turn `; } statusDiv.innerHTML = status; scoresTable.innerHTML = ''; for (let[id_in_group, num_cards] of data.card_counts) { let playerName = `Player ${id_in_group} `; if (id_in_group === js_vars.my_id) { playerName += ' (you)'; } scoresTable.insertAdjacentHTML('beforeend', ` < td >${playerName} < / td > < td >${num_cards} cards < / td > `); } } document.addEventListener('DOMContentLoaded', (event) = > { liveSend({'type': 'load'}); }); < / script > {{endblock}} word_search / __init__.py From otree - more - demos from otree.api import * import random from pathlib import Path doc = """ Multiplayer word search game """ def load_word_list(): # words from https://github.com/dovenokan/oxford-words return set(Path(__name__ + '/words.txt').read_text().split()) class C(BaseConstants): NAME_IN_URL = 'wordsearch' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 DIM = 5 NUM_SQUARES = DIM * DIM LEXICON = load_word_list() COORDS = [] for x in range(DIM): for y in range(DIM): COORDS.append((x, y)) class Subsession(BaseSubsession): pass class Group(BaseGroup): board = models.LongStringField() class Player(BasePlayer): score = models.IntegerField(initial=0) class FoundWord(ExtraModel): word = models.StringField() player = models.Link(Player) group = models.Link(Group) def word_in_board(word, board): lengths = list(range(1, len(word) + 1)) paths = {_: [] for _ in lengths} for i in range(C.DIM): for j in range(C.DIM): coord = (i, j) if board[coord] == word[0]: paths[1].append([coord]) for length in lengths[1:]: target_char = word[length - 1] for path in paths[length - 1]: cur_x, cur_y = path[-1] for dx in [-1, 0, 1]: for dy in [-1, 0, 1]: check_coord = (cur_x + dx, cur_y + dy) if ( check_coord in C.COORDS and board[check_coord] == target_char and check_coord not in path ): paths[length].append(path + [check_coord]) return bool(paths[len(word)]) def load_board(board_str): return dict(zip(C.COORDS, board_str.replace('\n', '').lower())) class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): rows = [] for _ in range(C.DIM): # add extra vowels row = ''.join( [random.choice('AAABCDEEEEEFGHIIKLMNNOOPRRSTTUUVWXYZ') for _ in range(C.DIM)] ) rows.append(row) group.board = '\n'.join(rows) def live_method(player: Player, data): group = player.group board = group.board if 'word' in data: word = data['word'].lower() is_in_board = len(word) >= 3 and word_in_board(word, load_board(board)) is_in_lexicon = is_in_board and word.lower() in C.LEXICON is_valid = is_in_board and is_in_lexicon already_found = is_valid and bool(FoundWord.filter(group=group, word=word)) success = is_valid and not already_found news = dict( word=word, success=success, is_in_board=is_in_board, is_in_lexicon=is_in_lexicon, already_found=already_found, id_in_group=player.id_in_group, ) if success: FoundWord.create(group=group, word=word) player.score += 1 else: news = {} scores = [[p.id_in_group, p.score] for p in group.get_players()] found_words = [fw.word for fw in FoundWord.filter(group=group)] return {0: dict(news=news, scores=scores, found_words=found_words)} class Play(Page): live_method = live_method timeout_seconds = 3 * 60 @staticmethod def vars_for_template(player: Player): group = player.group return dict(board=group.board.upper().split('\n')) @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) class Results(Page): pass page_sequence = [WaitToStart, Play, Results] word_search / Results.html From otree - more - demos \ \ {{block title}} Results {{endblock}} {{block content}} < p > < i > You can show the player 's results here...

{{endblock}} word_search / Play.html From otree - more - demos {{block title}} Word search {{endblock}} {{block content}} < style > .word - board { text - align: center; max - width: 15 em; } < / style > < table class ="table table-bordered word-board" > {{ for row in board}} < tr > {{ for char in row}} < td > {{char}} < / td > {{endfor}} < / tr > {{endfor}} < / table > < input id = "word" required minlength = "3" autofocus > < button type = "button" onclick = "sendWord()" > Send < / button > < div id = "news" > < / div > < br > < h5 > Words already found < / h5 > < p id = "found-words" > < / p > < h5 > Scores < / h5 > < table class ="table table-striped" > < tbody id = "scores-tbody" > < / tbody > < / table > < h5 > Instructions < / h5 > < ul > < li > Words can be in any direction including diagonal, and can change direction. < / li > < / ul > < script > let input = document.getElementById('word'); let newsDiv = document.getElementById('news'); let scoresBody = document.getElementById('scores-tbody'); let foundWords = document.getElementById('found-words'); input.addEventListener("keydown", function(event) { newsDiv.innerHTML = ''; if (event.key === "Enter") { sendWord(); } }); function sendWord() { if (input.reportValidity()) { liveSend({'word': input.value}); input.value = ''; } } function showNews(word, msg, success) { let color = success ? 'green': 'red'; newsDiv.innerHTML = ` < span style = "color: ${color}" >${word}: ${msg} < / span > `; } function liveRecv(data) { if ('news' in data) { let {success, is_in_board, already_found, is_in_lexicon, word, id_in_group} = data.news; if (id_in_group === js_vars.my_id) { let feedback; if (success) { feedback = '+1'; } else if (already_found) { feedback = 'Already found'; } else if (!is_in_board) { feedback = 'Not in board'; } else { feedback = 'Not in lexicon'; } showNews(word, feedback, success); } } scoresBody.innerHTML = ''; for (let[id_in_group, score] of data.scores) { let playerName = `Player ${id_in_group}`; if (id_in_group == = js_vars.my_id) { playerName += ' (you)'; } scoresBody.insertAdjacentHTML('beforeend', ` < td > ${playerName} < / td > < td > ${score} points < / td > `); } foundWords.innerHTML = data.found_words.join(', '); } document.addEventListener('DOMContentLoaded', (event) = > { liveSend({}); }); < / script > {{endblock}} intergenerational / __init__.py From otree - more - demos from otree.api import * class C(BaseConstants): NAME_IN_URL = 'intergenerational' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 MULTIPLIER = 1.8 MIN_AMOUNT = 2 class Subsession(BaseSubsession): locked = models.BooleanField(initial=False) cur_value = models.IntegerField(initial=100) generation = models.IntegerField(initial=0) def creating_session(subsession: Subsession): session = subsession.session session.intergenerational_history = [] def group_by_arrival_time_method(subsession: Subsession, waiting_players): if subsession.locked: return if len(waiting_players) >= 1: return [waiting_players[0]] class Group(BaseGroup): pass class Player(BasePlayer): value0 = models.IntegerField() value1 = models.IntegerField() value2 = models.IntegerField() extracted = models.IntegerField(label="Amount to extract", min=0) def extracted_max(player: Player): return player.value1 - C.MIN_AMOUNT class GBAT(WaitPage): group_by_arrival_time = True body_text = "You are in the queue..." @staticmethod def after_all_players_arrive(group: Group): subsession = group.subsession subsession.locked = True subsession.generation += 1 # actually just 1 player per group for player in group.get_players(): player.value0 = subsession.cur_value player.value1 = int(player.value0 * C.MULTIPLIER) class Extract(Page): form_model = 'player' form_fields = ['extracted'] timeout_seconds = 3 * 60 @staticmethod def before_next_page(player: Player, timeout_happened): subsession = player.subsession session = player.session player.value2 = player.value1 - player.extracted subsession.cur_value = player.value2 session.intergenerational_history.append( dict( generation=subsession.generation, value0=player.value0, value1=player.value1, extracted=player.extracted, value2=player.value2, ) ) subsession.locked = False class Results(Page): pass page_sequence = [GBAT, Extract, Results] intergenerational / Results.html From otree - more - demos {{block title}} Thank you {{endblock}} {{block content}} {{endblock}} intergenerational / Extract.html From otree - more - demos {{block title}}Extract {{endblock}} {{block content}} < p > This game is a simulation of how subsequent generations use a natural resource (cutting down a forest for lumber). < / p > < p > The forest will start out with 100 acres. Each generation, the size of the forest increases by a factor of {{C.MULTIPLIER}}. But each generation also has the opportunity to extract some amount of the forest for lumber. < / p > {{ if session.intergenerational_history}} < h3 > History < / h3 > < table class ="table" > < tr > < th > Generation < / th > < th > At period start(value0) < / th > < th > After growth(value1) < / th > < th > Extracted < / th > < th > Final amount(value2) < / th > < / tr > {{ for row in session.intergenerational_history}} < tr > < td > {{row.generation}} < / td > < td > {{row.value0}} < / td > < td > {{row.value1}} < / td > < td > {{row.extracted}} < / td > < td > {{row.value2}} < / td > < / tr > {{endfor}} < / table > {{endif}} < h3 > Current < / h3 > < p > During this generation the rainforest has grown from {{player.value0}} to < b > {{player.value1}} < / b > acres. < / p > < p > How much of this do you want to extract for the current generation? < / p > {{formfields}} {{next_button}} {{endblock}} bots_vs_humans / __init__.py From otree - more - demos from otree.api import * doc = """ Humans vs. bots. This is a public goods game where you play against 2 bots. The bots use a tit-for-tat strategy with some random noise. Note: this doesn't use oTree test bots, but rather an ExtraModel. It's simply a single-player game where random calculations are done on the server. You never need to open any links for the bots. """ class C(BaseConstants): NAME_IN_URL = 'bots_vs_humans' PLAYERS_PER_GROUP = None NUM_ROUNDS = 5 ENDOWMENT = cu(100) MULTIPLIER = 1.8 MAX_NOISE = 10 BOTS_PER_GROUP = 2 AGENTS_PER_GROUP = BOTS_PER_GROUP + 1 class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): for i in range(C.BOTS_PER_GROUP): MyBot.create(player=p, agent_id=i + 1) class Group(BaseGroup): pass class Player(BasePlayer): contrib = models.CurrencyField( min=0, max=C.ENDOWMENT, label="How much will you contribute?" ) agent_id = models.IntegerField(initial=1) group_total_contrib = models.CurrencyField() group_individual_share = models.CurrencyField() class MyBot(ExtraModel): player = models.Link(Player) # these fields should match what's defined on the Player, so that you can # loop over player and bot instances interchangeably. contrib = models.CurrencyField( min=0, max=C.ENDOWMENT, label="How much will you contribute?" ) payoff = models.CurrencyField() # replacement for id_in_group agent_id = models.IntegerField() def generate_contrib(mean): import random contrib = mean + random.randint(-C.MAX_NOISE, C.MAX_NOISE) # constrain it between 0 and endowment return max(0, min(contrib, C.ENDOWMENT)) def get_agents(player: Player): """'Agent' means either a bot or a human. This gets the user plus all bots""" return [player] + MyBot.filter(player=player) # FUNCTIONS def set_payoffs(player: Player): bots = MyBot.filter(player=player) if player.round_number == 1: mean = C.ENDOWMENT / 2 else: # tit-for-tat strategy based on last round actions. prev_player = player.in_round(player.round_number - 1) mean = prev_player.contrib for bot in bots: bot.contrib = generate_contrib(mean) agents = bots + [player] contribs = [p.contrib for p in agents] player.group_total_contrib = sum(contribs) player.group_individual_share = ( player.group_total_contrib * C.MULTIPLIER / C.AGENTS_PER_GROUP ) for ag in agents: ag.payoff = C.ENDOWMENT - ag.contrib + player.group_individual_share # PAGES class Contribute(Page): form_model = 'player' form_fields = ['contrib'] @staticmethod def before_next_page(player: Player, timeout_happened): set_payoffs(player) class WaitForBots(Page): """ This is just for show, to make it feel more realistic. Also, note it's a Page, not a WaitPage. Removing this page won't affect functionality. """ @staticmethod def get_timeout_seconds(player: Player): import random return random.randint(1, 5) class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(agents=get_agents(player)) page_sequence = [Contribute, WaitForBots, Results] bots_vs_humans / Results.html From otree - more - demos {{block title}}Results {{endblock}} {{block content}} < p > You started with an endowment of {{C.ENDOWMENT}}, of which you contributed {{player.contrib}}. Your group contributed {{player.group_total_contrib}}, resulting in an individual share of {{player.group_individual_share}}. Your profit is therefore {{player.payoff}}. < / p > < table class ="table table-striped" > < tr > < td > < / td > < th > Contribution < / th > < th > Payoff < / th > < / tr > {{ for ag in agents}} < tr > < td > {{ if ag == player}}You {{ else}} Bot {{ag.agent_id}} {{endif}} < / td > < td > {{ag.contrib}} < / td > < td > {{ag.payoff}} < / td > < / tr > {{endfor}} < / table > {{next_button}} {{endblock}} bots_vs_humans / Contribute.html From otree - more - demos {{block title}} Round {{subsession.round_number}} {{endblock}} {{block content}} < p > This is a multi - round public goods game where the {{C.BOTS_PER_GROUP}} other players in your group are < b > bots < / b >. The endowment is {{C.ENDOWMENT}}, and the efficiency factor is {{C.MULTIPLIER}}. < / p > {{formfields}} {{next_button}} {{endblock}} bots_vs_humans / WaitForBots.html From otree - more - demos {{block styles}} < style > .otree - timer { display: none; } < / style > {{endblock}} {{block content}} < div class ="container" > < div class ="card" > < h4 class ="card-header" > Please wait < / h4 > < div class ="card-body" > < p > Waiting for others to decide < / p > < div class ="progress" > < div class ="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%" > < / div > < / div > < / div > < / div > < / div > {{endblock}} image_annotation / __init__.py From otree - more - demos from otree.api import * class C(BaseConstants): NAME_IN_URL = 'image_annotation' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): decision = models.StringField( widget=widgets.RadioSelect, label="Based on this chart, what country would you say is the best to live in?", choices=['USA', 'Switzerland', 'Australia', 'Canada', 'Other'], ) annotations = models.LongStringField() image_path = models.StringField() class Annotation(ExtraModel): player = models.Link(Player) image_name = models.StringField() text = models.StringField() x = models.FloatField() y = models.FloatField() width = models.FloatField() height = models.FloatField() class Decide(Page): form_model = 'player' form_fields = ['decision', 'annotations'] @staticmethod def before_next_page(player: Player, timeout_happened): import json annos = json.loads(player.annotations) for anno in annos: geometry = anno['shapes'][0]['geometry'] image_name = anno['src'].split('/')[-1] Annotation.create(player=player, image_name=image_name, text=anno['text'], **geometry) # here we delete the annotations JSON to reduce bloat in the data export. # but you can remove this line if you want e.g. to re-display the annotations # on a separate page. player.annotations = '' class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(annotations=Annotation.filter(player=player)) page_sequence = [ Decide, Results, ] def custom_export(players): yield [ 'session_code', 'participant_code', 'image_name', 'text', 'x', 'y', 'width', 'height', ] for p in players: pp = p.participant session_code = pp.session.code for anno in Annotation.filter(player=p): yield [ session_code, pp.code, anno.image_name, anno.text, anno.x, anno.y, anno.width, anno.height, ] image_annotation / Results.html From otree - more - demos {{block title}} Thank you {{endblock}} {{block content}} < p > < i > The annotations are available in a custom data export. < / i > < / p > {{endblock}} image_annotation / Decide.html From otree - more - demos {{block title}} Image annotation {{endblock}} {{block content}} < link type = "text/css" rel = "stylesheet" href = "{{ static 'image_annotation/annotorious/css/annotorious.css' }}" / > < p > Let 's suppose you were considering moving to another country. Here is a chart that shows results of a survey where people were asked what other country they would most like to live in. < / p > < img src = "{{ static 'image_annotation/best_country_to_live.png' }}" width = "600px" class ="annotatable" / > {{ if form.annotations.errors}} < p class ="form-control-errors" > You must annotate the image.< / p > {{endif}} < p > < b > Add 1 - 3 annotations to the chart, to explain / support your decision (click and drag on part of the chart, then write your comment). < / b > < / p > < input type = "hidden" name = "annotations" / > {{formfield 'decision'}} {{next_button}} < script src = "{{ static 'image_annotation/annotorious/annotorious.min.js' }}" > < / script > < script > document.addEventListener("DOMContentLoaded", function(event) { anno.addHandler('onAnnotationCreated', function(annotation) { // don 't take annotations from images in history. // but this seems to exclude all annotations. // src is relative, but annotorious seems to record only // absolute URLs. let annotations = anno.getAnnotations(); for (let i = 0; i < annotations.length; i++) { // remove noise delete annotations[i].context; } let annotationsStr = JSON.stringify(annotations); // todo: replace with formfields document.getElementsByName('annotations')[0].value = annotationsStr; }); }); < / script > {{endblock}} go_no_go / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} You made {{player.num_errors}} errors. {{ if avg_reaction_ms}} Your average reaction time was {{avg_reaction_ms}} ms. {{endif}} {{endblock}} go_no_go / Introduction.html From otree - more - demos {{block title}} Instructions {{endblock}} {{block content}} < p > You will be shown a series of images in quick succession. If the image is green, press the '1' key as quickly as possible. If the image is red, don 't press anything. Instead, wait for the next image. < / p > < p > When you are ready to start, press the button below. < / p > < button class ="btn btn-primary" > Start < / button > {{endblock}} wait_page_from_scratch / __init__.py From otree - more - demos from otree.api import * from json import dumps as json_dumps, loads as json_loads doc = """ Wait page implemented from scratch, using live pages. """ class C(BaseConstants): NAME_IN_URL = 'wait_page_from_scratch' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): # json fields wait_for_ids = models.LongStringField(initial='[]') arrived_ids = models.LongStringField(initial='[]') def unarrived_players(group: Group): return set(json_loads(group.wait_for_ids)) - set(json_loads(group.arrived_ids)) class Player(BasePlayer): pass class Intro(Page): pass def wait_page_live_method(player: Player, data): group = player.group arrived_ids_set = set(json_loads(group.arrived_ids)) arrived_ids_set.add(player.id_in_subsession) group.arrived_ids = json_dumps(list(arrived_ids_set)) if not unarrived_players(group): return {0: dict(finished=True)} class ScratchWaitPage(Page): @staticmethod def is_displayed(player: Player): group = player.group # first time if not json_loads(group.wait_for_ids): wait_for_ids = [p.id_in_subsession for p in group.get_players()] group.wait_for_ids = json_dumps(wait_for_ids) return unarrived_players(group) @staticmethod def live_method(player: Player, data): if data.get('type') == 'wait_page': return wait_page_live_method(player, data) @staticmethod def error_message(player: Player, values): group = player.group if unarrived_players(group): return "Wait page not finished" class Results(Page): pass page_sequence = [Intro, ScratchWaitPage, Results] wait_page_from_scratch / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > < i > Next page content would go here... < / i > < / p > {{endblock}} wait_page_from_scratch / Intro.html From otree - more - demos {{block title}} Intro {{endblock}} {{block content}} < p > Simple wait page implemented from scratch, using live pages. You can use this if you need functionality that differs from that offered by the built - in WaitPage, such as making the wait page show who is still waiting, using a live_method on the wait page, having a timeout on the wait page, etc. < / p > < p > There are {{C.PLAYERS_PER_GROUP}} players per group. < / p > < p > Click next. < / p > {{next_button}} {{endblock}} wait_page_from_scratch / ScratchWaitPage.html From otree - more - demos {{block content}} < div class ="container" > < div class ="card" > < h4 class ="card-header" > Please wait < / h4 > < div class ="card-body" > < p > Waiting for others< / p > < div class ="progress" > < div class ="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%" > < / div > < / div > < / div > < / div > < / div > < script > function liveRecv(data) { console.log('data', data) if (data.finished) { document.getElementById("form").submit(); } } document.addEventListener("DOMContentLoaded", (event) = > { liveSend({'type': 'wait_page'}); }); < / script > {{endblock}} image_rating / __init__.py From otree - more - demos from otree.api import * doc = """ Rating images (WTP/willingness to pay) """ def read_csv(): import csv f = open(__name__ + '/catalog.csv', encoding='utf-8-sig') rows = [row for row in csv.DictReader(f)] for row in rows: row['image_path'] = 'grocery/{}.png'.format(row['image_png']) return rows class C(BaseConstants): NAME_IN_URL = 'image_rating' PLAYERS_PER_GROUP = None PRODUCTS = read_csv() NUM_ROUNDS = len(PRODUCTS) class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): img = get_current_product(p) p.sku = img['sku'] class Group(BaseGroup): pass class Player(BasePlayer): sku = models.StringField() willingness_to_pay = models.CurrencyField(label="", min=0) def get_current_product(player: Player): return C.PRODUCTS[player.round_number - 1] class MyPage(Page): form_model = 'player' form_fields = ['willingness_to_pay'] @staticmethod def vars_for_template(player: Player): return dict(product=get_current_product(player)) class Results(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS page_sequence = [MyPage, Results] image_rating / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > < i > You can show or tabulate the user 's results here, using player.in_all_rounds(), etc.

{{endblock}} image_rating / MyPage.html From otree - more - demos {{block content}} < p > How much would you pay for the following product? < / p > < h5 class ="card-title" > {{product.name}} < / h5 > < img src = "{{ static product.image_path }}" style = "width: 50px" > {{formfields}} {{next_button}} < script > // this part is optional.it automatically puts the cursor in the text box, // so that the user doesn 't need to click on it. document.getElementsByName('willingness_to_pay')[0].focus(); < / script > {{endblock}} panas / Survey.html From otree - more - demos {{block title}} Positive and Negative Affect Schedule(PANAS - SF) {{endblock}} {{block content}} < p > For each word, indicate the extent you have felt this way over the past week. < / p > < table class ="table table-striped" > < tr > < th > < / th > < th > 1 < br > Very slightly < br > or not at all < / th > < th > 2 < br > A little < / th > < th > 3 < br > Moderately < / th > < th > 4 < br > Quite a bit < / th > < th > 5 < br > Extremely < / th > < / tr > {{ for field in form}} < tr > < th > {{field.label}} < / th > {{ for option in field}} < td > {{option}} < / td > {{endfor}} < / tr > {{endfor}} < / table > {{next_button}} < script > // workaround needed until wtforms # 615 is published for (let option of document.querySelectorAll('input[type=radio]')) { option.required = 'required'; } < / script > {{endblock}} panas / __init__.py From otree - more - demos from otree.api import * doc = """ PANAS (positive and negative affect schedule) """ class C(BaseConstants): NAME_IN_URL = 'panas' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 FIELDS = [ 'interested', 'distressed', 'excited', 'upset', 'strong', 'guilty', 'scared', 'hostile', 'enthusiastic', 'proud', 'irritable', 'alert', 'ashamed', 'inspired', 'nervous', 'determined', 'attentive', 'jittery', 'active', 'afraid', ] POSITIVE_FIELDS = [ 'interested', 'excited', 'strong', 'enthusiastic', 'proud', 'alert', 'inspired', 'determined', 'attentive', 'active', ] NEGATIVE_FIELDS = [ 'distressed', 'upset', 'guilty', 'scared', 'hostile', 'irritable', 'ashamed', 'nervous', 'jittery', 'afraid', ] # make sure each field is counted exactly once assert sorted(FIELDS) == sorted(POSITIVE_FIELDS + NEGATIVE_FIELDS) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass def make_q(label): return models.IntegerField(label=label, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelect) class Player(BasePlayer): interested = make_q('Interested') distressed = make_q('Distressed') excited = make_q('Excited') upset = make_q('Upset') strong = make_q('Strong') guilty = make_q('Guilty') scared = make_q('Scared') hostile = make_q('Hostile') enthusiastic = make_q('Enthusiastic') proud = make_q('Proud') irritable = make_q('Irritable') alert = make_q('Alert') ashamed = make_q('Ashamed') inspired = make_q('Inspired') nervous = make_q('Nervous') determined = make_q('Determined') attentive = make_q('Attentive') jittery = make_q('Jittery') active = make_q('Active') afraid = make_q('Afraid') positive_affect_score = models.IntegerField() negative_affect_score = models.IntegerField() # PAGES class Survey(Page): form_model = 'player' form_fields = C.FIELDS @staticmethod def before_next_page(player: Player, timeout_happened): player.positive_affect_score = sum(getattr(player, f) for f in C.POSITIVE_FIELDS) player.negative_affect_score = sum(getattr(player, f) for f in C.NEGATIVE_FIELDS) class Results(Page): pass page_sequence = [Survey, Results] panas / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > Your positive affect score is {{player.positive_affect_score}}. < / p > < p > Your negative affect score is {{player.negative_affect_score}}. < / p > {{endblock}} twitter / __init__.py From otree - more - demos from otree.api import * doc = """ Mini-Twitter """ class C(BaseConstants): NAME_IN_URL = 'twitter' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): pass def get_follower_ids(player: Player, including_myself): ids = [sub.follower_id_in_group for sub in Subscription.filter(leader=player)] if including_myself: ids.append(player.id_in_group) return ids class Subscription(ExtraModel): # we store the player objects and id_in_group redundantly, # for convenience and performance leader = models.Link(Player) leader_id_in_group = models.IntegerField() follower = models.Link(Player) follower_id_in_group = models.IntegerField() class Message(ExtraModel): player = models.Link(Player) player_id_in_group = models.IntegerField() group = models.Link(Group) text = models.LongStringField() def to_dict(msg: Message): return dict(id_in_group=msg.player_id_in_group, text=msg.text, ) def live_method(player: Player, data): group = player.group my_id = player.id_in_group msg_type = data['type'] if msg_type == 'write': text = data['text'] msg = Message.create(player=player, player_id_in_group=my_id, text=text, group=group) followers = get_follower_ids(player, including_myself=True) return {follower: dict(messages=[to_dict(msg)]) for follower in followers} broadcast = {} if msg_type == 'toggle_follow': leader_id = data['id_in_group'] leader = group.get_player_by_id(leader_id) subs = Subscription.filter(follower=player, leader=leader) if subs: [sub] = subs sub.delete() else: Subscription.create( leader=leader, leader_id_in_group=leader.id_in_group, follower=player, follower_id_in_group=my_id, ) # notify the other person of the change to their followers broadcast[leader_id] = dict(followers=get_follower_ids(leader, including_myself=True)) followers = get_follower_ids(player, including_myself=True) i_follow = [sub.leader_id_in_group for sub in Subscription.filter(follower=player)] i_dont_follow = [p.id_in_group for p in group.get_players() if p.id_in_group not in i_follow] unfiltered_messages = Message.filter(group=group) # i see my own messages in my feed my_feed_authors = i_follow + [my_id] messages = [to_dict(m) for m in unfiltered_messages if m.player_id_in_group in my_feed_authors] broadcast.update( { my_id: dict( full_load=True, followers=followers, i_follow=i_follow, i_dont_follow=i_dont_follow, messages=messages, ) } ) return broadcast # PAGES class MyPage(Page): live_method = live_method @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) page_sequence = [MyPage] twitter / Results.html From otree - more - demos {{block title}} Page title {{endblock}} {{block content}} {{next_button}} {{endblock}} twitter / MyPage.html From otree - more - demos {{block title}} Player {{player.id_in_group}} {{endblock}} {{block content}} < h4 > Write something < / h4 > < p > < input type = "text" id = "new-message" size = "60" > < button type = "button" onclick = "sendMsg()" > Post < / button > < / p > < br > < h4 > My feed < / h4 > < div id = "feed" > < / div > < br > < h4 > My followers < / h4 > < ul id = "followers" > < / ul > < br > < h4 > People I follow < / h4 > < table id = "i-follow" class ="table" > < / table > < br > < h4 > People I don 't follow < table id = "i-dont-follow" class ="table" > < / table > < br > < script > let newMessage = document.getElementById('new-message'); let feed = document.getElementById('feed'); let followers = document.getElementById('followers'); let i_follow = document.getElementById('i-follow'); let i_dont_follow = document.getElementById('i-dont-follow'); function sendMsg() { let text = newMessage.value.trim(); if (text) { liveSend({'type': 'write', 'text': text}) } newMessage.value = ''; } function toggle_follow(btn) { liveSend({'type': 'toggle_follow', 'id_in_group': parseInt(btn.value)}); } newMessage.addEventListener("keydown", function(event) { if (event.key === "Enter") { sendMsg(); } }); function getNickname(id_in_group) { return id_in_group === js_vars.my_id ? 'Me': `Player ${id_in_group} `; } function makeUserTable(ids_in_group, table_ele, am_following) { let btnLabel = am_following ? 'Unfollow': 'Follow'; table_ele.innerHTML = ''; for (let iig of ids_in_group) { if (iig === js_vars.my_id) continue; let nickname = getNickname(iig); let row = ` < tr > < td >${nickname} < / td > < td > < button type = "button" value = "${iig}" onclick = "toggle_follow(this)" > ${btnLabel} < / button > < / td > < / tr > `; table_ele.insertAdjacentHTML('beforeend', row) } } function liveRecv(data) { if (data.full_load) { makeUserTable(data.i_follow, i_follow, true); makeUserTable(data.i_dont_follow, i_dont_follow, false); feed.innerHTML = ''; } if ('messages' in data) { for (let msg of data.messages) { let msgSpan = document.createElement('span'); msgSpan.textContent = msg.text; let sender = getNickname(msg.id_in_group); let row = ` < div > < b > ${sender} < / b >: $ {msgSpan.innerHTML} < / div > `; feed.insertAdjacentHTML('afterbegin', row); } } if ('followers' in data) { followers.innerHTML = ''; for (let iig of data.followers) { if (iig === js_vars.my_id) continue; let nickname = getNickname(iig); let row = ` < li >${nickname} < / li > `; followers.insertAdjacentHTML('beforeend', row); } } } document.addEventListener("DOMContentLoaded", function(event) { liveSend({'type': 'load'}); }); < / script > {{endblock}} nim / __init__.py From otree - more - demos from otree.api import * doc = """ Game of Nim. Players take turns adding a number. First to 15 wins. """ class C(BaseConstants): NAME_IN_URL = 'nim' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 TARGET = 15 class Subsession(BaseSubsession): pass class Group(BaseGroup): current_number = models.IntegerField(initial=1) whose_turn = models.IntegerField(initial=1) winner_id = models.IntegerField() game_over = models.BooleanField(initial=False) class Player(BasePlayer): is_winner = models.BooleanField(initial=False) # PAGES class Game(Page): @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def live_method(player: Player, number): group = player.group my_id = player.id_in_group other_id = 3 - my_id if ( # if the number is non-null etc. number and number in [1, 2, 3] and group.whose_turn == my_id # if you're at 14, you can't choose 3. and group.current_number + number <= C.TARGET ): group.current_number += number news = dict(id_in_group=my_id, number=number) if group.current_number == C.TARGET: group.winner_id = player.id_in_group group.game_over = True else: group.whose_turn = other_id else: news = None return { 0: dict( game_over=group.game_over, current_number=group.current_number, whose_turn=group.whose_turn, news=news, ) } class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): winner = group.get_player_by_id(group.winner_id) winner.is_winner = True class Results(Page): pass page_sequence = [Game, ResultsWaitPage, Results] nim / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} {{ if player.is_winner}} You won! {{ else}} you lost: ( {{endif}} {{endblock}} nim / Game.html From otree-more-demos {{block title}} Nim {{endblock}} {{block content}} < p id="news" > < / p > < p > Current number: < / p > < div id="current_number" > < / div > < p > How much to add: < / p > < button type="button" class ="btn-step" onclick="liveSend(1)" > 1 < / button > < button type = "button" class ="btn-step" onclick="liveSend(2)" > 2 < / button > < button type = "button" class ="btn-step" onclick="liveSend(3)" > 3 < / button > < script > let newsEle = document.getElementById('news'); let current_number = document.getElementById('current_number'); function liveRecv(data) { if (data.game_over) { document.getElementById('form').submit(); } let lastActor; let btnDisabledStatus; if (data.whose_turn === js_vars.my_id) { lastActor = 'The other player'; btnDisabledStatus = '' } else { lastActor = 'You'; btnDisabledStatus = 'disabled' } for (let btn of document.getElementsByClassName('btn-step')) { btn.disabled = btnDisabledStatus; } current_number.innerText = data.current_number; let news = data.news; if (news) { newsEle.innerText = `${lastActor} added ${news.number}`; } } window.addEventListener('DOMContentLoaded', (event) = > { liveSend(null); }); < / script > < p > This is a game of nim.You and the other take turns adding to a number. The game starts at 1, and on each turn you can add 1, 2, or 3. The player who reaches {{C.TARGET}} wins the game. < / p > {{endblock}} fast_consensus / __init__.py From otree - more - demos from otree.api import * import time doc = """ Reach a consensus with your group before your payoffs shrink to 0. Similar to the "Endgame" segment of the British game show "Divided": https://www.youtube.com/watch?v=8k8ETko16tQ """ class C(BaseConstants): NAME_IN_URL = 'fast_consensus' PLAYERS_PER_GROUP = 3 NUM_ROUNDS = 1 RANKS = [ dict(number=1, payoff=cu(800), label="Gold"), dict(number=2, payoff=cu(300), label="Silver"), dict(number=3, payoff=cu(100), label="Bronze"), ] TIMEOUT_SECONDS = 60 * 2 class Subsession(BaseSubsession): pass class Group(BaseGroup): deadline = models.FloatField() reached_consensus = models.BooleanField(initial=False) fraction_of_original = models.FloatField() def seconds_left(group: Group): return group.deadline - time.time() class Player(BasePlayer): proposed_rank = models.IntegerField() final_rank = models.IntegerField() rank_label = models.StringField() # PAGES class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): group.deadline = time.time() + C.TIMEOUT_SECONDS class Negotiate(Page): @staticmethod def live_method(player: Player, data): group = player.group players = group.get_players() if group.reached_consensus: return {0: dict(finished=True)} if 'proposed_rank' in data: rank = data['proposed_rank'] player.proposed_rank = rank if set(p.field_maybe_none('proposed_rank') for p in players) == set( d['number'] for d in C.RANKS ): payoffs = {d['number']: d['payoff'] for d in C.RANKS} labels = {d['number']: d['label'] for d in C.RANKS} fraction_of_original = round(seconds_left(group) / C.TIMEOUT_SECONDS, 4) group.fraction_of_original = fraction_of_original group.reached_consensus = True for p in players: p.final_rank = p.proposed_rank p.rank_label = labels[p.final_rank] p.payoff = payoffs[p.final_rank] * fraction_of_original return {0: dict(finished=True)} return { 0: dict(ranks=[[p.id_in_group, p.field_maybe_none('proposed_rank')] for p in players]) } @staticmethod def js_vars(player: Player): group = player.group return dict( my_id=player.id_in_group, deadline=group.deadline, RANKS=C.RANKS, TIMEOUT_SECONDS=C.TIMEOUT_SECONDS, ) @staticmethod def get_timeout_seconds(player: Player): import time group = player.group return group.deadline - time.time() class ResultsWaitPage(WaitPage): pass class Results(Page): pass page_sequence = [WaitToStart, Negotiate, ResultsWaitPage, Results] fast_consensus / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} {{ if group.reached_consensus}} < p > Congratulations! Your group reached a consensus.Here is the results table: < / p > < table class ="table" > < tr > < th > Player < / th > < th > Rank < / th > < th > Payoff < / th > < / tr > {{ for p in group.get_players()}} < tr > < td > Player {{p.id_in_group}} {{ if p == player}}(me) {{endif}} < / td > < td > {{p.rank_label}} < / td > < td > {{p.payoff}} < / td > < / tr > {{endfor}} < / table > {{ else}} < p > Sorry, you did not reach a consensus.Your payoff is 0. < / p > {{endif}} {{next_button}} {{endblock}} iowa_gambling / instructions.html From otree - more - demos < div class ="card" > < div class ="card-body" > < h5 class ="card-title" > Instructions < / h5 > < p class ="card-text" > In this task, you are presented with 4 decks of cards. Each card you turn over has a reward and may have a cost also. Your task is to turn over {{C.NUM_TRIALS}} cards, with the goal of making the highest payoff. Note: Each deck has a different distribution of rewards and costs. < / p > < / div > < / div > iowa_gambling / Play.html From otree - more - demos {{block title}} Progress: < span id = "task-progress" > < / span > / {{C.NUM_TRIALS}} {{endblock}} {{block content}} < div class ="container" > < table class ="table" > < colgroup > < col style = "width: 50%" / > < col style = "width: 50%" / > < / colgroup > < tr > < td > Your total payoff < / td > < td id = "cum_payoff" > < / td > < / tr > < tr > < td > Last card reward < / td > < th id = "reward" style = "color: green" > < / th > < / tr > < tr > < td > Last card cost < / td > < th id = "cost" style = "color: red" > < / th > < / tr > < / table > < div class ="row" > {{ for letter in 'ABCD'}} < div class ="col" > < button type = "button" onclick = "selectDeck(this)" value = "{{ letter }}" class ="btn-card" > < !-- it 's just a coincidence that we use the bootstrap ' card ' element to represent a card :) --> < div class ="card" style="width: 10rem; height: 14rem" > < div class ="card-body" > < h2 class ="card-title" > Deck < / h2 > < h1 class ="card-title" > {{letter}} < / h1 > < / div > < / div > < / button > < / div > {{endfor}} < / div > < br > < br > {{include_sibling 'instructions.html'}} < / div > < script > let buttons = document.getElementsByClassName('btn-card'); let msgCost = document.getElementById('cost'); let msgReward = document.getElementById('reward'); let msgCumPayoff = document.getElementById('cum_payoff'); let msgProgress = document.getElementById('task-progress'); function selectDeck(btn) { liveSend({'letter': btn.value}); for (let btn of buttons) { btn.disabled = 'disabled'; } } function liveRecv(data) { if ('finished' in data) { document.getElementById('form').submit(); return; } console.log(data); if ('reward' in data) { // unpack let cost = data.cost; let reward = data.reward; msgReward.innerHTML = cu(reward); msgCost.innerHTML = cost == = 0 ? '': cu(cost); } msgCumPayoff.innerHTML = cu(data.cum_payoff); msgProgress.innerHTML = data.num_trials; for (let btn of buttons) { btn.disabled = ''; } } function cu(amount) { return `${amount} points `; } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > {{endblock}} fast_consensus / Negotiate.html From otree - more - demos {{block title}} Negotiation {{endblock}} {{block content}} < p > Based on your performances in the previous game, you need to now agree on ranks, which will determine your payoffs. Two people cannot have the same rank. You have {{C.TIMEOUT_SECONDS}} seconds to come to an agreement. During the negotiation, all payoffs will shrink down gradually and will hit 0 if a consensus is not reached in time. < / p > < p > This table shows in real time the ranks each player gives themselves. < / p > < table class ="table" > < tr > < th > < / th > {{ for d in C.RANKS}} < th > {{d.label}}( < span id = "cur-payoff-{{ d.number }}" > < / span > points) < / th > {{endfor}} < / tr > {{ for p in group.get_players()}} < tr > < th > Participant {{p.id_in_group}} {{ if p == player}}(me) {{endif}} < / th > {{ for d in C.RANKS}} < td > < input type = "radio" name = "rank{{ p.id_in_group }}" onclick = "sendRank(this)" value = "{{ d.number }}" id = "radio{{ d.number }}" {{ if p != player}}disabled {{endif}} > < / td > {{endfor}} < / tr > {{endfor}} < / table > < div class ="chat-widget" > < b > Chat with your group < / b > {{chat}} < / div > < script > let curPayoffDivs = js_vars.RANKS.map(n= > document.getElementById(`cur - payoff -${n.number} `)); let form = document.getElementById('form'); function sendRank(radio) { liveSend({'proposed_rank': parseInt(radio.value)}) } function liveRecv(data) { if (data.finished) { document.getElementById('form').submit(); return; } for (let[id_in_group, rank] of data.ranks) { if (rank === null) continue; form[`rank${id_in_group} `].value = rank; } } // update the shrinking payoffs every second setInterval( function() { let seconds_left = Math.max(0, js_vars.deadline - Date.now() / 1000); let frac = seconds_left / js_vars.TIMEOUT_SECONDS; for (let d of js_vars.RANKS) { curPayoffDivs[d.number - 1].innerText = Math.round(d.payoff * frac); } }, 1000 ) document.addEventListener('DOMContentLoaded', (event) = > { liveSend({}); }); < / script > {{endblock}} iowa_gambling / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > < i > See the admin data table for the user's stats

{{endblock}} iowa_gambling / __init__.py From otree-more-demos from otree.api import * doc = """ Iowa Gambling Task. See: "Insensitivity to future consequences following damage to human prefrontal cortex" (Bechara et al, 1994) """ class C(BaseConstants): NAME_IN_URL = 'iowa' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 # randomization is done within a block of 10 trials, see figure 1 of the 1994 paper BLOCK_SIZE = 10 NUM_BLOCKS = 10 # should be greater than block_size * num_blocks NUM_TRIALS = 50 # in the classic game it is 100 # these are the rewards for each deck, which are constant REWARDS = [100, 100, 50, 50] class Subsession(BaseSubsession): pass def generate_block(): import random costs = [[150, 200, 250, 300, 350], [1250], [50, 50, 50, 50, 50], [250]] for ele in costs: # add zeroes until it has 10 elements ele += [0] * (C.BLOCK_SIZE - len(ele)) random.shuffle(ele) return costs def creating_session(subsession: Subsession): session = subsession.session import random costs = [[], [], [], []] for i in range(C.NUM_BLOCKS): block = generate_block() costs[0] += block[0] costs[1] += block[1] costs[2] += block[2] costs[3] += block[3] session.iowa_costs = costs for p in subsession.get_players(): deck_layout = ['A', 'B', 'C', 'D'] random.shuffle(deck_layout) p.deck_layout = ''.join(deck_layout) class Group(BaseGroup): pass class Player(BasePlayer): # how many they have selected from each deck num0 = models.IntegerField(initial=0) num1 = models.IntegerField(initial=0) num2 = models.IntegerField(initial=0) num3 = models.IntegerField(initial=0) num_trials = models.IntegerField(initial=0) deck_layout = models.StringField() def live_method(player: Player, data): my_id = player.id_in_group session = player.session # data will contain A, B, C, D # guard if player.num_trials == C.NUM_TRIALS: return {my_id: dict(finished=True)} resp = {} if 'letter' in data: letter = data['letter'] deck = player.deck_layout.index(letter) field_name = 'num{}'.format(deck) cur_count = getattr(player, field_name) reward = C.REWARDS[deck] cost = session.iowa_costs[deck][cur_count] payoff = reward - cost player.payoff += payoff cur_count += 1 setattr(player, field_name, cur_count) player.num_trials += 1 resp.update(cost=cost, reward=reward) if player.num_trials == C.NUM_TRIALS: resp.update(finished=True) resp.update(cum_payoff=player.payoff, num_trials=player.num_trials) return {my_id: resp} class Play(Page): live_method = live_method class Results(Page): pass page_sequence = [Play, Results] ebay / __init__.py From otree - more - demos from otree.api import * import time doc = """ eBay style auction """ class C(BaseConstants): NAME_IN_URL = 'ebay' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): auction_end = models.FloatField() # start with player 1 by default highest_bidder = models.IntegerField(initial=1) highest_bid = models.CurrencyField(initial=0) class Player(BasePlayer): is_winner = models.BooleanField(initial=False) bid = models.CurrencyField(initial=0) # PAGES class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): group.auction_end = time.time() + 60 class Bidding(Page): @staticmethod def live_method(player: Player, data): group = player.group my_id = player.id_in_group is_new_high_bid = False if 'bid' in data: bid = data['bid'] if bid > group.highest_bid: player.bid = bid group.highest_bid = bid group.highest_bidder = my_id is_new_high_bid = True return { 0: dict( is_new_high_bid=is_new_high_bid, highest_bid=group.highest_bid, highest_bidder=group.highest_bidder, ) } @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def get_timeout_seconds(player: Player): import time group = player.group return group.auction_end - time.time() class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): winner = group.get_player_by_id(group.highest_bidder) winner.is_winner = True # you lose whatever you bid. winner.payoff = -group.highest_bid class Results(Page): pass page_sequence = [WaitToStart, Bidding, ResultsWaitPage, Results] ebay / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > Your bid was {{player.bid}}. < / p > {{ if player.is_winner}} < p > This was the highest bid.Therefore you win! < / p > {{ else}} < p > The highest bid was {{group.highest_bid}}. Therefore, you did not win the item. < / p > {{endif}} {{endblock}} ebay / Bidding.html From otree - more - demos {{block title}} Auction {{endblock}} {{block content}} < p > This is an auction for a Ronaldo autographed jersey.< / p > < table class ="table" > < tr > < th > Highest bid < / th > < td id = "highest-bid" > < / td > < / tr > < tr > < th > Make new bid < / th > < td > < input type = "number" id = "input-bid" > < button type = "button" onclick = "sendBid()" > Make new bid < / button > < / td > < / tr > < / table > < script > let bidInput = document.getElementById('input-bid'); let highestBidDiv = document.getElementById('highest-bid'); bidInput.addEventListener("keydown", function(event) { if (event.key === "Enter") { sendBid(); } }); function sendBid() { let bid = parseInt(bidInput.value); if (isNaN(bid)) return; liveSend({'bid': bid}) bidInput.value = ''; } function cu(amount) { return `${amount} points `; } function liveRecv(data) { // javascript destructuring assignment let {is_new_high_bid, highest_bid, highest_bidder} = data; let highestBidderLabel = (highest_bidder === js_vars.my_id) ? 'Me': `Player ${highest_bidder} `; highestBidDiv.innerText = `${cu(highest_bid)}(${highestBidderLabel})`; if (is_new_high_bid) { highestBidDiv.style.backgroundColor = 'lightgreen'; setTimeout(function () { highestBidDiv.style.backgroundColor = ''; }, 1000); } } window.addEventListener('DOMContentLoaded', (event) = > { liveSend({}); }); < / script > {{endblock}} strategy_method / __init__.py From otree - more - demos from otree.api import * doc = """ Strategy method for ultimatum game. """ class C(BaseConstants): NAME_IN_URL = 'strategy_method' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 ENDOWMENT = cu(100) OFFER_CHOICES = currency_range(0, ENDOWMENT, 10) OFFER_CHOICES_COUNT = len(OFFER_CHOICES) POSSIBLE_ALLOCATIONS = [] for OFFER in OFFER_CHOICES: POSSIBLE_ALLOCATIONS.append(dict(p1_amount=OFFER, p2_amount=ENDOWMENT - OFFER)) class Subsession(BaseSubsession): pass def make_strategy_field(number): return models.BooleanField( label="Would you accept an offer of {}?".format(cu(number)), widget=widgets.RadioSelectHorizontal, # note to self: remove this once i release bugfix choices=[[False, 'No'], [True, 'Yes']], ) class Group(BaseGroup): amount_offered = models.CurrencyField(choices=C.OFFER_CHOICES, ) offer_accepted = models.BooleanField() # another way to implement this game would be with an ExtraModel, instead of making # all these hardcoded fields. # that's what the choice_list app does. # that would be more flexible, but also more complex since you would have to implement the # formfields yourself with HTML and Javascript. # in this case, since the rules of the game are pretty simple, # and there are not too many fields, # just defining these hardcoded fields is fine. response_0 = make_strategy_field(0) response_10 = make_strategy_field(10) response_20 = make_strategy_field(20) response_30 = make_strategy_field(30) response_40 = make_strategy_field(40) response_50 = make_strategy_field(50) response_60 = make_strategy_field(60) response_70 = make_strategy_field(70) response_80 = make_strategy_field(80) response_90 = make_strategy_field(90) response_100 = make_strategy_field(100) def set_payoffs(group: Group): p1, p2 = group.get_players() amount_offered = group.amount_offered group.offer_accepted = getattr(group, 'response_{}'.format(int(amount_offered))) if group.offer_accepted: p1.payoff = C.ENDOWMENT - amount_offered p2.payoff = amount_offered else: p1.payoff = 0 p2.payoff = 0 class Player(BasePlayer): pass class P1(Page): form_model = 'group' form_fields = ['amount_offered'] @staticmethod def is_displayed(player: Player): return player.id_in_group == 1 class P2(Page): form_model = 'group' form_fields = ['response_{}'.format(int(i)) for i in C.OFFER_CHOICES] @staticmethod def is_displayed(player: Player): return player.id_in_group == 2 class ResultsWaitPage(WaitPage): after_all_players_arrive = set_payoffs title_text = "Thank you" body_text = ( "You can close this page. When the other player arrives, the payoff will be calculated." ) class Results(Page): pass page_sequence = [ P1, P2, ResultsWaitPage, Results, ] strategy_method / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > {{ if player.id_in_group == 1}} You were given {{C.ENDOWMENT}}, out of which you offered {{group.amount_offered}} to the other player. {{ if group.offer_accepted}} Your offer was accepted. {{ else}} Your offer was rejected. {{endif}} {{ else}} The other player offered you {{group.amount_offered}} out of the total {{C.ENDOWMENT}}. Based on your preferences, this offer was {{ if group.offer_accepted}} accepted. {{ else}} rejected. {{endif}} {{endif}} < / p > < p > Your payoff is therefore {{player.payoff}}. < / p > {{include_sibling 'instructions.html'}} {{endblock}} dollar_auction / Intro.html From otree - more - demos {{block title}} Intro {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} {{next_button}} {{endblock}} dollar_auction / Results.html From otree - more - demos {{block title}} Page title {{endblock}} {{block content}} {{ if player.is_top_bidder}} You were the top bidder.Your bid was {{group.top_bid}}. {{ elif player.is_second_bidder}} You were the second bidder.Your bid was {{group.second_bid}}. {{ else}} You were not the top bidder or the second bidder. {{endif}} Therefore, your final payoff is {{player.payoff}}. {{endblock}} strategy_method / P1.html From otree - more - demos {{block title}} Your role and decision {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} < p > You are the < b > proposer < / b >. < / p > < p > The two of you have been given {{C.ENDOWMENT}}. Enter the amount that you want to offer to the responder. < / p > {{formfields}} {{next_button}} {{endblock}} strategy_method / P2.html From otree - more - demos {{block title}} Your role and decision {{endblock}} {{block content}} {{include_sibling 'instructions.html'}} < p > You are the < b > responder < / b >. < / p > < p > The proposer will make you an offer on how to divide the {{C.ENDOWMENT}}. < / p > < p > Please choose below how you would respond to each of the possible offers. < / p > {{formfields}} {{next_button}} {{endblock}} strategy_method / instructions.html From otree - more - demos < h3 > Instructions < / h3 > < div > < p > You have been paired with another player. One player is the proposer and once is the responder. < / p > < p > The two of you will together receive {{C.ENDOWMENT}}. The proposer will make the responder an offer, which the responder can accept or reject. If the offer is rejected, both will receive nothing. < / p > < h4 > Proposer 's role < / h4 > < p > The proposer has the following {{C.OFFER_CHOICES_COUNT}} options on how to divide the {{C.ENDOWMENT}} between the two players. < / p > < ol > {{ for a in C.POSSIBLE_ALLOCATIONS}} < li > {{a.p2_amount}} for the responder(the proposer keeps {{a.p1_amount}}) < / li > {{endfor}} < / ol > < h4 > Responder 's role < / h4 > < p > While the proposer makes the offer, the responder can select which {{C.OFFER_CHOICES_COUNT}} offers stated above to accept or reject. After the proposer has made the offer, it will be either accepted or rejected according to the responder 's choices. < / p > < / div > live_bargaining / Bargain.html From otree - more - demos {{block title}} Make a deal {{endblock}} {{block content}} < p > This is a 2 - player game.A seller and a buyer are negotiating for the price of [insert item here]. < / p > < p > You are the {{player.role}}. Please negotiate with the {{other_role}} below. < / p > < table class ="table" > < tr > < th > Your current proposal < / th > < td id = "my-proposal" > (none) < / td > < td > < input type = "number" id = "my_offer" > < button type = "button" onclick = "sendOffer()" id = "btn-offer" > Make new offer < / button > < / td > < / tr > < tr > < th > {{other_role}} proposal < / th > < td id = "other-proposal" > (none) < / td > < td > < button type = "button" id = "btn-accept" onclick = "sendAccept(this)" style = "display: none" > Accept < / button > < / td > < / tr > < / table > < h4 > Chat < / h4 > {{chat nickname = player.role}} < script > let my_offer = document.getElementById('my_offer'); let btnAccept = document.getElementById('btn-accept'); let msgOtherProposal = document.getElementById('other-proposal'); let msgMyProposal = document.getElementById('my-proposal'); let otherProposal; my_offer.addEventListener("keydown", function(event) { if (event.key === "Enter") { sendOffer(); } }); function sendOffer() { liveSend({'type': 'propose', 'amount': my_offer.value}) my_offer.value = ''; } function sendAccept() { liveSend({'type': 'accept', 'amount': otherProposal}) } function cu(amount) { return `${amount} points `; } function liveRecv(data) { if ('proposals' in data) { for (let[id_in_group, proposal] of data.proposals) { if (id_in_group == = js_vars.my_id) { msgMyProposal.innerHTML = cu(proposal) } else { msgOtherProposal.innerHTML = cu(proposal); otherProposal = proposal; btnAccept.style.display = 'block'; } } } if ('finished' in data) { document.getElementById('form').submit(); } } window.addEventListener('DOMContentLoaded', (event) = > { liveSend({}); }); < / script > {{endblock}} live_bargaining / __init__.py From otree - more - demos from otree.api import * doc = """ For oTree beginners, it would be simpler to implement this as a discrete-time game by using multiple rounds, e.g. 10 rounds, where in each round both players can make a new proposal, or accept the value from the previous round. However, the discrete-time version has more limitations (fixed communication structure, limited number of iterations). Also, the continuous-time version works smoother & faster, and is less resource-intensive since it all takes place in 1 page. """ class C(BaseConstants): NAME_IN_URL = 'live_bargaining' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 SELLER_ROLE = 'Seller' BUYER_ROLE = 'Buyer' class Subsession(BaseSubsession): pass class Group(BaseGroup): deal_price = models.CurrencyField() is_finished = models.BooleanField(initial=False) class Player(BasePlayer): amount_proposed = models.IntegerField() amount_accepted = models.IntegerField() class Bargain(Page): @staticmethod def vars_for_template(player: Player): return dict(other_role=player.get_others_in_group()[0].role) @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def live_method(player: Player, data): group = player.group [other] = player.get_others_in_group() if 'amount' in data: try: amount = int(data['amount']) except Exception: print('Invalid message received', data) return if data['type'] == 'accept': if amount == other.amount_proposed: player.amount_accepted = amount group.deal_price = amount group.is_finished = True return {0: dict(finished=True)} if data['type'] == 'propose': player.amount_proposed = amount proposals = [] for p in [player, other]: amount_proposed = p.field_maybe_none('amount_proposed') if amount_proposed is not None: proposals.append([p.id_in_group, amount_proposed]) return {0: dict(proposals=proposals)} @staticmethod def error_message(player: Player, values): group = player.group if not group.is_finished: return "Game not finished yet" @staticmethod def is_displayed(player: Player): """Skip this page if a deal has already been made""" group = player.group deal_price = group.field_maybe_none('deal_price') return deal_price is None class Results(Page): pass page_sequence = [Bargain, Results] live_bargaining / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > Congratulations, you made a deal at the price of {{group.deal_price}}. < / p > {{endblock}} choice_list / table.html From otree - more - demos < table class ="table table-striped" > < tr > < th > < / th > < th > Or... < / th > < th > < / th > < / tr > {{ for t in trials}} < tr > < td > {{t.sure_payoff}} with a probability of 100 % < / td > < td > {{ if not is_results}} < input type = "radio" name = "{{ t.id }}" value = "0" required onclick = "sendPref(this)" > < input type = "radio" name = "{{ t.id }}" value = "1" required onclick = "sendPref(this)" > {{endif}} < / td > < td > {{t.lottery_high}} with a probability of {{t.probability_percent}} %, {{t.lottery_low}} otherwise. < / td > < / tr > {{endfor}} < / table > choice_list / __init__.py From otree - more - demos from otree.api import * doc = """ Choice list (Holt/Laury, risk preferences, price list, equivalence test, etc) """ class C(BaseConstants): NAME_IN_URL = 'choice_list' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 def read_csv(): import csv import random f = open(__name__ + '/stimuli.csv', encoding='utf-8-sig') rows = list(csv.DictReader(f)) random.shuffle(rows) return rows class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): for p in subsession.get_players(): stimuli = read_csv() for stim in stimuli: # In python, ** unpacks a dict. Trial.create(player=p, **stim) class Group(BaseGroup): pass class Player(BasePlayer): chose_lottery = models.BooleanField() won_lottery = models.BooleanField() class Trial(ExtraModel): player = models.Link(Player) sure_payoff = models.CurrencyField() lottery_high = models.CurrencyField() lottery_low = models.CurrencyField() probability_percent = models.IntegerField() chose_lottery = models.BooleanField() is_selected = models.BooleanField(initial=False) # PAGES class Stimuli(Page): @staticmethod def vars_for_template(player: Player): return dict(trials=Trial.filter(player=player), is_results=False) @staticmethod def live_method(player: Player, data): # In this case, Trial.filter() will return a list with just 1 item. # so we use python 'iterable unpacking' to assign that single item # to the variable 'trial'. [trial] = Trial.filter(player=player, id=data['trial_id']) trial.chose_lottery = data['chose_lottery'] @staticmethod def before_next_page(player: Player, timeout_happened): import random # if your page has a timeout, you would need to adjust this code. trials = Trial.filter(player=player) selected_trial = random.choice(trials) selected_trial.is_selected = True player.chose_lottery = selected_trial.chose_lottery if player.chose_lottery: player.won_lottery = selected_trial.probability_percent > (random.random() * 100) if player.won_lottery: payoff = selected_trial.lottery_high else: payoff = selected_trial.lottery_low else: payoff = selected_trial.sure_payoff player.payoff = payoff class Results(Page): @staticmethod def vars_for_template(player: Player): trials = Trial.filter(player=player, is_selected=True) return dict(trials=trials, is_results=True) page_sequence = [Stimuli, Results] choice_list / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > Here is the row that was randomly chosen. < / p > {{include_sibling 'table.html'}} < p > {{ if player.chose_lottery}} You chose( and {{ if player.won_lottery}}won {{ else}}lost {{endif}}) the lottery, {{ else}} You chose the sure payoff, {{endif}} so your payoff is {{player.payoff}}. < / p > {{endblock}} choice_list / Stimuli.html From otree - more - demos {{block title}} {{endblock}} {{block content}} < p > Make a choice for each of the below rows. A random row will be chosen and you will be paid according to your choice for that row. < / p > {{include_sibling 'table.html'}} {{next_button}} < script > function sendPref(radio) { liveSend({trial_id: radio.name, chose_lottery: radio.value == = '1'}); } < / script > {{endblock}} dollar_auction / __init__.py From otree - more - demos from otree.api import * doc = """ Dollar auction """ class C(BaseConstants): NAME_IN_URL = 'dollar_auction' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 JACKPOT = 100 class Subsession(BaseSubsession): pass class Group(BaseGroup): top_bid = models.CurrencyField(initial=0) second_bid = models.CurrencyField(initial=0) top_bidder = models.IntegerField(initial=-1) second_bidder = models.IntegerField(initial=-1) auction_timeout = models.FloatField() def get_state(group: Group): return dict( top_bid=group.top_bid, top_bidder=group.top_bidder, second_bid=group.second_bid, second_bidder=group.second_bidder, ) class Player(BasePlayer): is_top_bidder = models.BooleanField(initial=False) is_second_bidder = models.BooleanField(initial=False) class Intro(Page): pass class WaitToStart(WaitPage): @staticmethod def after_all_players_arrive(group: Group): import time group.auction_timeout = time.time() + 60 # PAGES class Bid(Page): @staticmethod def get_timeout_seconds(player: Player): import time group = player.group return group.auction_timeout - time.time() @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group) @staticmethod def live_method(player: Player, bid): group = player.group my_id = player.id_in_group if bid: if bid > group.top_bid: group.second_bid = group.top_bid group.second_bidder = group.top_bidder group.top_bid = bid group.top_bidder = my_id return {0: dict(get_state(group), new_top_bid=True)} else: return {my_id: get_state(group)} class ResultsWaitPage(WaitPage): @staticmethod def after_all_players_arrive(group: Group): if group.top_bidder > 0: top_bidder = group.get_player_by_id(group.top_bidder) top_bidder.payoff = C.JACKPOT - group.top_bid top_bidder.is_top_bidder = True if group.second_bidder > 0: second_bidder = group.get_player_by_id(group.second_bidder) second_bidder.payoff = -group.second_bid second_bidder.is_second_bidder = True class Results(Page): pass page_sequence = [Intro, WaitToStart, Bid, ResultsWaitPage, Results] punishment / Punish.html From otree - more - demos {{block title}}Punishment stage {{endblock}} {{block content}} < p > You contributed {{player.contribution}}. Here are the other players ' contributions: < / p > < table class ="table" > {{ for p in other_players}} < tr > < td > Player {{p.id_in_group}} < / td > < td > {{p.contribution}} < / td > < / tr > {{endfor}} < / table > < p > You can punish each player up to {{C.MAX_PUNISHMENT}} points. Each punishment point will reduce the player 's payoff by 10%, but will incur a cost to you, according to the table at the bottom. < / p > {{formfields}} < table class ="table table-striped" > < tr > < th > Punishment points < / th > < th > Cost to you < / th > < / tr > {{ for entry in schedule}} < tr > < td > {{entry .0}} < / td > < td > {{entry .1 | cu}} < / td > < / tr > {{endfor}} < / table > {{next_button}} {{endblock}} punishment / Contribute.html From otree - more - demos {{block title}}Contribute {{endblock}} {{block content}} < p > This is a public goods game with {{C.PLAYERS_PER_GROUP}} players per group, an endowment of {{C.ENDOWMENT}}, and an efficiency factor of {{C.MULTIPLIER}}. < / p > {{formfields}} {{next_button}} {{endblock}} punishment / Results.html From otree - more - demos {{block title}}Results {{endblock}} {{block content}} < table class ="table" > < tr > < th > Your contribution < / th > < td > {{player.contribution}} < / td > < / tr > < tr > < th > Group total contribution < / th > < td > {{group.total_contribution}} < / td > < / tr > < tr > < th > Group individual share < / th > < td > {{group.individual_share}} < / td > < / tr > < tr > < th > Punishment received < / th > < td > {{player.punishment_received}} < / td > < / tr > < tr > < th > Self cost of punishment < / th > < td > {{player.cost_of_punishing}} < / td > < / tr > < tr > < th > Your payoff < / th > < td > {{player.payoff}} < / td > < / tr > < / table > {{endblock}} punishment / __init__.py From otree - more - demos from otree.api import * doc = """ Public goods with punishment, roughly based on Fehr & Gaechter 2000. """ class C(BaseConstants): NAME_IN_URL = 'punishment' PLAYERS_PER_GROUP = 4 NUM_ROUNDS = 3 ENDOWMENT = cu(20) MULTIPLIER = 1.6 MAX_PUNISHMENT = 10 PUNISHMENT_SCHEDULE = { 0: 0, 1: 1, 2: 2, 3: 4, 4: 6, 5: 9, 6: 12, 7: 16, 8: 20, 9: 25, 10: 30, } class Subsession(BaseSubsession): pass class Group(BaseGroup): total_contribution = models.CurrencyField() individual_share = models.CurrencyField() def make_punishment_field(id_in_group): return models.IntegerField( min=0, max=C.MAX_PUNISHMENT, label="Punishment to player {}".format(id_in_group) ) class Player(BasePlayer): contribution = models.CurrencyField( min=0, max=C.ENDOWMENT, label="How much will you contribute?" ) punish_p1 = make_punishment_field(1) punish_p2 = make_punishment_field(2) punish_p3 = make_punishment_field(3) punish_p4 = make_punishment_field(4) cost_of_punishing = models.CurrencyField() punishment_received = models.CurrencyField() def get_self_field(player: Player): return 'punish_p{}'.format(player.id_in_group) def punishment_fields(player: Player): return ['punish_p{}'.format(p.id_in_group) for p in player.get_others_in_group()] def set_payoffs(group: Group): players = group.get_players() contributions = [p.contribution for p in players] group.total_contribution = sum(contributions) group.individual_share = group.total_contribution * C.MULTIPLIER / C.PLAYERS_PER_GROUP for p in players: payoff_before_punishment = C.ENDOWMENT - p.contribution + group.individual_share self_field = get_self_field(p) punishments_received = [getattr(other, self_field) for other in p.get_others_in_group()] p.punishment_received = min(10, sum(punishments_received)) punishments_sent = [getattr(p, field) for field in punishment_fields(p)] p.cost_of_punishing = sum(C.PUNISHMENT_SCHEDULE[points] for points in punishments_sent) p.payoff = payoff_before_punishment * (1 - p.punishment_received / 10) - p.cost_of_punishing # PAGES class Contribute(Page): form_model = 'player' form_fields = ['contribution'] class WaitPage1(WaitPage): pass class Punish(Page): form_model = 'player' get_form_fields = punishment_fields @staticmethod def vars_for_template(player: Player): return dict( other_players=player.get_others_in_group(), schedule=C.PUNISHMENT_SCHEDULE.items(), ) class WaitPage2(WaitPage): after_all_players_arrive = set_payoffs class Results(Page): pass page_sequence = [ Contribute, WaitPage1, Punish, WaitPage2, Results, ] svo / instructions.html From otree - more - demos < h3 > Instructions < / h3 > < p > In this task you have been randomly paired with another person, whom we will refer to as the other.This other person is someone you do not know and will remain mutually anonymous.All of your choices are completely confidential.You will be making a series of decisions about allocating resources between you and this other person. For each of the following questions, please indicate the distribution you prefer most by marking the respective position along the midline.You can only make one mark for each question. < / p > < p > Your decisions will yield money for both yourself and the other person.In the example below, a person has chosen to distribute money so that he / she receives 50 points, while the anonymous other person receives 40 points. < / p > < p > There are no right or wrong answers, this is all about personal preferences.After you have made your decision, write the resulting distribution of money on the spaces on the right.As you can see, your choices will influence both the amount of money you receive as well as the amount of money the other receives. < / p > bigfive / Survey.html From otree - more - demos {{block title}} Personality test {{endblock}} {{block content}} < p > Please evaluate the following statements, to complete the sentence: < b > "I see myself as someone who...". < / p > < table class ="table table-striped" > < tr > < th > < / th > < th > Disagree strongly < / th > < th > Disagree a little < / th > < th > Neither agree nor disagree < / th > < th > Agree a little < / th > < th > Agree strongly < / th > < / tr > {{ for field in form}} < tr > < th > {{field.label}} < / th > {{ for option in field}} < td > {{option}} < / td > {{endfor}} < / tr > {{endfor}} < / table > {{next_button}} < script > // workaround needed until wtforms # 615 is published for (let option of document.querySelectorAll('input[type=radio]')) { option.required = 'required'; } < / script > {{endblock}} bigfive / __init__.py From otree - more - demos from otree.api import * doc = """Big 5 personality test""" class C(BaseConstants): NAME_IN_URL = 'bigfive' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass def make_q(label): return models.IntegerField(label=label, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelect) class Player(BasePlayer): q1 = make_q('is reserved') q2 = make_q('is generally trusting') q3 = make_q('tends to be lazy') q4 = make_q('is relaxed, handles stress well') q5 = make_q('has few artistic interests') q6 = make_q('is outgoing, sociable') q7 = make_q('tends to find fault with others') q8 = make_q('does a thorough job') q9 = make_q('gets nervous easily') q10 = make_q('has an active imagination') extraversion = models.FloatField() agreeableness = models.FloatField() conscientiousness = models.FloatField() neuroticism = models.FloatField() openness = models.FloatField() def combine_score(positive, negative): return 3 + (positive - negative) / 2 class Survey(Page): form_model = 'player' form_fields = ['q1', 'q2', 'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9', 'q10'] @staticmethod def before_next_page(player: Player, timeout_happened): player.extraversion = combine_score(player.q6, player.q1) player.agreeableness = combine_score(player.q2, player.q7) player.conscientiousness = combine_score(player.q8, player.q3) player.neuroticism = combine_score(player.q9, player.q4) player.openness = combine_score(player.q10, player.q5) class Results(Page): pass page_sequence = [Survey, Results] bigfive / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < table class ="table" > < tr > < td > Extraversion < / td > < td > {{player.extraversion}} / 5 < / td > < / tr > < tr > < td > Agreeableness < / td > < td > {{player.agreeableness}} / 5 < / td > < / tr > < tr > < td > Conscientiousness < / td > < td > {{player.conscientiousness}} / 5 < / td > < / tr > < tr > < td > Neuroticism < / td > < td > {{player.neuroticism}} / 5 < / td > < / tr > < tr > < td > Openness < / td > < td > {{player.openness}} / 5 < / td > < / tr > < / table > {{endblock}} wisconsin / __init__.py From otree - more - demos from pprint import pprint # noqa from otree.api import * import random doc = """ Wisconsin card sorting test: https://doi.org/10.1093/cercor/1.1.62 """ class C(BaseConstants): NAME_IN_URL = 'wisconsin' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 SWITCH_THRESHOLD = 6 NUM_TRIALS = 30 RULES = ['color', 'shape', 'number'] COLORS = ['red', 'blue', 'green', 'yellow'] SHAPES = ['circle', 'triangle', 'star', 'plus'] NUMBERS = [1, 2, 3, 4] class Subsession(BaseSubsession): matching_rules = models.LongStringField() def creating_session(subsession: Subsession): for p in subsession.get_players(): p.layout = random_layout() p.rule = random.choice(C.RULES) class Group(BaseGroup): pass class Player(BasePlayer): # how many they have selected from each deck num_trials = models.IntegerField(initial=0) num_correct = models.IntegerField(initial=0) num_correct_in_block = models.IntegerField(initial=0) rule = models.StringField(doc="Either color, shape, or number") layout = models.StringField() is_finished = models.BooleanField(initial=False) def random_layout(): # color, number, shape, and 0 (which means nothing matches) layout = list('cns0') random.shuffle(layout) return ''.join(layout) def generate_decks(test_card: dict, layout): cs = [c for c in C.COLORS if c != test_card['color']] ss = [c for c in C.SHAPES if c != test_card['shape']] ns = [c for c in C.NUMBERS if c != test_card['number']] random.shuffle(cs) random.shuffle(ss) random.shuffle(ns) decks = [] for letter in layout: if letter == 'c': card = dict(color=test_card['color'], shape=ss.pop(), number=ns.pop()) elif letter == 's': card = dict(shape=test_card['shape'], color=cs.pop(), number=ns.pop()) elif letter == 'n': card = dict(number=test_card['number'], shape=ss.pop(), color=cs.pop()) else: card = dict(color=cs.pop(), shape=ss.pop(), number=ns.pop()) decks.append(card) return decks def live_method(player: Player, data): print('player.rule', player.rule) print('player.layout', player.layout) my_id = player.id_in_group # guard if player.is_finished: return {my_id: dict(finished=True)} resp = {} if 'deck_number' in data: deck = data['deck_number'] # [0] means the first letter of the rule. is_correct = player.layout[deck] == player.rule[0] player.num_trials += 1 player.num_correct += is_correct player.num_correct_in_block += is_correct if player.num_correct_in_block == C.SWITCH_THRESHOLD: other_rules = [r for r in C.RULES if r != player.rule] player.rule = random.choice(other_rules) player.num_correct_in_block = 0 # layout changes each turn. otherwise, the user could just keep # clicking on the same box for the rest of the block. player.layout = random_layout() feedback = '✓' if is_correct else '✗' resp.update(feedback=feedback) test_card = dict( color=random.choice(C.COLORS), shape=random.choice(C.SHAPES), number=random.choice(C.NUMBERS), ) decks = generate_decks(test_card, player.layout) resp.update(test_card=test_card, decks=decks) if player.num_trials == C.NUM_TRIALS: player.is_finished = True resp.update(finished=True) resp.update(num_trials=player.num_trials) return {my_id: resp} class Play(Page): live_method = live_method @staticmethod def vars_for_template(player: Player): return dict(deck_numbers=range(4)) @staticmethod def error_message(player: Player, values): if not player.is_finished: return "Game not finished" class Results(Page): pass page_sequence = [Play, Results] wisconsin / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > < i > See the admin data table for the user's stats

{{endblock}} wisconsin / Play.html From otree - more - demos {{block title}} Progress: < span id = "task-progress" > < / span > / {{C.NUM_TRIALS}} {{endblock}} {{block content}} < style > .wisc - card { width: 10rem; height: 14 rem; align - items: center; justify - content: center; } .wisc - img { display: block; } # feedback { font - size: x - large; } < / style > < div class ="container" > < p id = "feedback" > < / p > < div class ="row" > {{ for deck_number in deck_numbers}} < div class ="col" > < button type = "button" onclick = "selectDeck(this)" value = "{{ deck_number }}" class ="btn-card" > < !-- it 's just a coincidence that we use the bootstrap ' card ' element to represent a card :) --> < div class ="card wisc-card" > < div class ="card-body" id="deck-{{deck_number}}" > < / div > < / div > < / button > < / div > {{endfor}} < / div > < br > < p > Match the below card to one of the above cards. < / p > < div class ="card wisc-card" > < div class ="card-body" id="test-card" > < / div > < / div > < br > < br > {{include_sibling 'instructions.html'}} < / div > < script > let buttons = document.getElementsByClassName('btn-card'); let msgProgress = document.getElementById('task-progress'); let msgFeedback = document.getElementById('feedback'); function selectDeck(btn) { liveSend({'deck_number': parseInt(btn.value)}); for (let btn of buttons) { btn.disabled = 'disabled'; } } function makeCardContent({number, shape, color}) { let images = []; for (let i = 0; i < number; i++) { let image = ` < img src="/static/wisconsin/${shape}-${color}.svg" width="50em" class ="wisc-img" > `; images.push(image); } return images.join(''); } function liveRecv(data) { if ('finished' in data) { document.getElementById('form').submit(); return; } if ('feedback' in data) { msgFeedback.innerHTML = data.feedback; } if ('decks' in data) { for (let i = 0; i < 4; i++) { document.getElementById(`deck -${i}`).innerHTML = makeCardContent(data.decks[i]) } } if ('test_card' in data) { document.getElementById(`test - card`).innerHTML = makeCardContent(data.test_card); } msgProgress.innerHTML = data.num_trials; for (let btn of buttons) { btn.disabled = ''; } } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > {{endblock}} wisconsin / instructions.html From otree - more - demos \ < div class ="card" > < div class ="card-body" > < h5 class ="card-title" > Instructions < / h5 > < p class ="card-text" > You need to guess which of the above 4 decks each card matches with. The matching is based on either the color, shape, or number of items in the card. After each guess, you will be told whether your guess was right. However, the matching rule will periodically change throughout the game. < / p > < / div > < / div > stroop / Task.html From otree - more - demos {{block title}} {{endblock}} {{block content}} {{ for path in image_paths}} < img class ="stroopimage" src="{{ static path }}" style="display: none" > {{endfor}} < div id = "lastresult" style = "font-size: 100px" > < / div > < div id = "loading" > Get ready... < / div > < script > let image_id; let images = document.getElementsByClassName('stroopimage'); let lastresult = document.getElementById('lastresult'); let displayed_timestamp; let loading = document.getElementById('loading'); // time before we unhideDiv the first image(give time to get hands ready on keyboard) const INITIAL_DELAY = 1000; // time in between showing showing ✓ or ✗, and showing the next image const IN_BETWEEN_DELAY = 1000; function liveRecv(data) { for (let image of images) { image.style.display = 'none'; } if (data.feedback) lastresult.innerHTML = data.feedback; lastresult.style.display = 'block'; if (data.is_finished) { document.getElementById('form').submit(); } else { image_id = data.image_id; setTimeout(loadImage, IN_BETWEEN_DELAY); } } function loadImage() { lastresult.style.display = 'none'; images[image_id].style.display = 'block'; isRefractoryPeriod = false; displayed_timestamp = performance.now(); } let isRefractoryPeriod = false; document.addEventListener("keypress", function(event) { let color = js_vars.color_keys[event.key]; if (isRefractoryPeriod) return; isRefractoryPeriod = true; if (color) { liveSend({ submission: color, image_id: image_id, displayed_timestamp: displayed_timestamp, answered_timestamp: performance.now() }) } }); document.addEventListener('DOMContentLoaded', function(event) { setTimeout(function() { loading.style.display = 'none'; liveSend({}); }, INITIAL_DELAY); }); < / script > {{endblock}} stroop / __init__.py From otree - more - demos from pathlib import Path from otree.api import * doc = """ Stroop test. """ def get_permutations(): colors = C.COLORS idx = 0 items = [] for decoy_text in colors: for color in colors: items.append( dict( decoy_text=decoy_text, color=color, image_id=idx, is_congruent=decoy_text == color, ) ) idx += 1 return items def randomize_order(): import random permutations = get_permutations() random.shuffle(permutations) return permutations class C(BaseConstants): NAME_IN_URL = 'stroop' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 COLORS = ['red', 'yellow', 'blue', 'green'] COLOR_KEYS = [('r', 'red'), ('y', 'yellow'), ('b', 'blue'), ('g', 'green')] NUM_TRIALS = len(COLORS) * len(COLORS) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): num_completed = models.IntegerField(initial=0) num_correct = models.IntegerField(initial=0) avg_congruent = models.FloatField() avg_incongruent = models.FloatField() incongruent_minus_congruent = models.FloatField() num_page_loads = models.IntegerField( initial=0, doc="""If more than 1, indicates that the user reloaded the page. This could change the interpretation of the timestamps.""", ) class Trial(ExtraModel): player = models.Link(Player) image_id = models.IntegerField() decoy_text = models.StringField() color = models.StringField() is_correct = models.BooleanField() is_congruent = models.BooleanField() reaction_ms = models.IntegerField() def get_current_trial(player: Player): return Trial.filter(player=player, is_correct=None)[0] def is_finished(player: Player): return player.num_completed == C.NUM_TRIALS # FUNCTIONS def creating_session(subsession: Subsession): for p in subsession.get_players(): for permutation in randomize_order(): Trial.create(player=p, **permutation) def live_method(player: Player, data): if data: if is_finished(player): return trial = get_current_trial(player) # guard against double-clicks if data['image_id'] != trial.image_id: return displayed_timestamp = data['displayed_timestamp'] answered_timestamp = data['answered_timestamp'] trial.submission = data['submission'] trial.is_correct = trial.submission == trial.color trial.reaction_ms = answered_timestamp - displayed_timestamp if trial.is_correct: player.num_correct += 1 feedback = '✓' else: feedback = '✗' player.num_completed += 1 else: feedback = '' if is_finished(player): return {player.id_in_group: dict(is_finished=True)} payload = dict(feedback=feedback, image_id=get_current_trial(player).image_id) return {player.id_in_group: payload} # PAGES class Introduction(Page): pass class Task(Page): live_method = live_method @staticmethod def vars_for_template(player: Player): player.num_page_loads += 1 image_paths = ['stroop/{}.png'.format(i) for i in range(C.NUM_TRIALS)] return dict(image_paths=image_paths) @staticmethod def js_vars(player: Player): return dict(color_keys=dict(C.COLOR_KEYS)) @staticmethod def before_next_page(player: Player, timeout_happened): from statistics import mean congruent_times = [] incongruent_times = [] for trial in Trial.filter(player=player): if trial.is_congruent: lst = congruent_times else: lst = incongruent_times lst.append(trial.reaction_ms) player.avg_congruent = int(mean(congruent_times)) player.avg_incongruent = int(mean(incongruent_times)) player.incongruent_minus_congruent = player.avg_incongruent - player.avg_congruent class Results(Page): pass page_sequence = [Introduction, Task, Results] stroop / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < table class ="table" > < tr > < th > Number of correct answers < / th > < td > {{player.num_correct}} / {{C.NUM_TRIALS}} < / td > < / tr > < tr > < th > Average for congruent< / th > < td > {{player.avg_congruent}} ms < / td > < / tr > < tr > < th > Average for incongruent< / th > < td > {{player.avg_incongruent}} ms < / td > < / tr > < tr > < th > Difference < / th > < td > {{player.incongruent_minus_congruent}} ms < / td > < / tr > < / table > {{endblock}} asynchronous / __init__.py From otree - more - demos from otree.api import * doc = """ Asynchronous 2-player sequential game (players can play at different times), where we guarantee players never have to wait. The example used is an ultimatum game. The first player who arrives is assigned to be P1. If the next player arrives after P1 has made a decision, he will be paired with P1 and see P1's decision. Otherwise, he will be P1 in a new group. Worst-case scenario is that all players arrive around the same time, and therefore everyone gets assigned to be P1. This game doesn't use oTree groups. Rather, it stores the partner's ID in a player field. """ class C(BaseConstants): NAME_IN_URL = 'asynchronous' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 ENDOWMENT = cu(100) class Subsession(BaseSubsession): pass def creating_session(subsession: Subsession): session = subsession.session # queue of players who are finished. session.finished_p1_list = [] class Group(BaseGroup): pass class Player(BasePlayer): is_p1 = models.BooleanField() offer = models.CurrencyField( min=0, max=C.ENDOWMENT, label="What's your offer to Player 2?", doc="This field is only used if the player is P1", ) partner_id = models.IntegerField( doc="This field is only used if the player is P2. It stores the ID of P1", ) accepted = models.BooleanField( label="Do you accept Player 1's offer?", doc="This field is only used if the player is P2", ) class Intro(Page): @staticmethod def before_next_page(player: Player, timeout_happened): # 'group' is not the 2-player group, but rather the default oTree # group of all players in the session. group = player.group session = player.session finished_p1_list = session.finished_p1_list # if someone already finished, assign the current player # to be P2 if finished_p1_list: player.is_p1 = False player.partner_id = finished_p1_list.pop() p1 = group.get_player_by_id(player.partner_id) p1.partner_id = player.id_in_group else: player.is_p1 = True class P1(Page): @staticmethod def is_displayed(player: Player): return player.is_p1 form_model = 'player' form_fields = ['offer'] @staticmethod def before_next_page(player: Player, timeout_happened): session = player.session # indicate that the player has finished so that he can be paired # with the next p2. session.finished_p1_list.append(player.id_in_group) class P1ThankYou(Page): @staticmethod def is_displayed(player: Player): return player.is_p1 class P2(Page): @staticmethod def is_displayed(player: Player): return not player.is_p1 form_model = 'player' form_fields = ['accepted'] @staticmethod def vars_for_template(player: Player): group = player.group p1 = group.get_player_by_id(player.partner_id) return dict(p1=p1) @staticmethod def before_next_page(player: Player, timeout_happened): group = player.group p1 = group.get_player_by_id(player.partner_id) if player.accepted: player.payoff = p1.offer p1.payoff = C.ENDOWMENT - p1.offer else: player.payoff = 0 p1.payoff = 0 class P2Results(Page): @staticmethod def is_displayed(player: Player): return not player.is_p1 @staticmethod def vars_for_template(player: Player): group = player.group p1 = group.get_player_by_id(player.partner_id) return dict(p1=p1) page_sequence = [ Intro, P1, P1ThankYou, P2, P2Results, ] asynchronous / P2Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > You {{ if player.accepted}} accepted {{ else}} rejected {{endif}} P1 's offer of {{ p1.offer }}. < / p > < p > Therefore, your payoff is {{player.payoff}}. < / p > {{next_button}} {{endblock}} asynchronous / P1.html From otree - more - demos {{block title}} Player 1 {{endblock}} {{block content}} < p > You are Player 1. Your endowment is {{C.ENDOWMENT}}. < / p > {{formfields}} {{next_button}} {{endblock}} asynchronous / Intro.html From otree - more - demos {{block title}} Ultimatum game {{endblock}} {{block content}} < p > This is an ultimatum game. You are Player 1. You have an endowment of {{C.ENDOWMENT}}. You can offer any portion of this to Player 2. If Player 2 accepts your offer, you both get paid according to your split. If Player 2 rejects the offer, you both get paid nothing. < / p > < p > < i > What 's special about this game is that rather than playing with another player live, you will be paired with someone who already played earlier. If nobody already played earlier, you will start a new group. < / i > < / p > {{next_button}} {{endblock}} asynchronous / P2.html From otree - more - demos {{block title}} Player 2 {{endblock}} {{block content}} < p > You are Player 2. Player 1 offered you {{p1.offer}}. < / p > {{formfields}} {{next_button}} {{endblock}} asynchronous / P1ThankYou.html From otree - more - demos {{block title}} Thank you {{endblock}} {{block content}} < p > Thank you for participating. If and when Player 2 joins the group, your payoff will be determined. < / p > {{endblock}} svo / chart.html From otree - more - demos < script src = "https://code.highcharts.com/highcharts.js" > < / script > < script src = "https://code.highcharts.com/modules/series-label.js" > < / script > < div id = "highchart" > < / div > < script > function redrawChart(to_self, to_other) { Highcharts.chart('highchart', { chart: { type: 'bar' }, title: null, xAxis: { categories: ['You', 'Other player'], title: { text: null } }, yAxis: { min: 0, max: 100, labels: { overflow: 'justify' }, title: { text: "Reward (in points)" } }, plotOptions: { series: { animation: false } }, credits: { enabled: false }, series: [{ data: [to_self, to_other], showInLegend: false, }] }); } < / script > svo / __init__.py From otree - more - demos import csv import math from otree.api import * doc = """ Social Value Orientation (Murphy et al) """ def read_csv(): with open('svo/SVO.csv', encoding='utf-8-sig') as f: return [ dict( round_number=int(row['round_number']), to_self=int(row['to_self']), to_other=int(row['to_other']), ) for row in csv.DictReader(f) ] def group_rows(): from collections import defaultdict d = defaultdict(list) for row in read_csv(): round_number = row['round_number'] d[round_number].append(row) return d class C(BaseConstants): NAME_IN_URL = 'svo' PLAYERS_PER_GROUP = None NUM_ROUNDS = 15 # change to 15 for the full task ROWS = group_rows() class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): choice = models.IntegerField(choices=list(range(9))) angle = models.FloatField() category = models.StringField() def assign_category(angle): if angle > 57.15: return 'Altruistic' if angle > 22.45: return 'Prosocial' if angle > -12.04: return 'Individualistic' return 'Competitive' class Decide(Page): form_model = 'player' form_fields = ['choice'] @staticmethod def js_vars(player: Player): return dict(rows=C.ROWS[player.round_number]) class Results(Page): @staticmethod def is_displayed(player: Player): return player.round_number == C.NUM_ROUNDS @staticmethod def vars_for_template(player: Player): participant = player.participant to_self_total = 0 to_other_total = 0 for p in player.in_all_rounds(): row = C.ROWS[p.round_number][p.choice] to_self_total += row['to_self'] to_other_total += row['to_other'] avg_self = to_self_total / C.NUM_ROUNDS avg_other = to_other_total / C.NUM_ROUNDS radians = math.atan((avg_other - 50) / (avg_self - 50)) participant.svo_angle = round(math.degrees(radians), 2) participant.svo_category = assign_category(participant.svo_angle) page_sequence = [Decide, Results] svo / Results.html From otree - more - demos {{block title}} Results {{endblock}} {{block content}} < p > Your SVO angle is {{participant.svo_angle}} degrees, so your category is {{participant.svo_category}}. < / p > {{endblock}} svo / Decide.html From otree - more - demos {{block title}} SVO task: item {{subsession.round_number}} of {{C.NUM_ROUNDS}} {{endblock}} {{block content}} < p > Choose a distribution between yourself and the other player. < / p > < div class ="input-group" > < input type = "range" name = "choice" min = "0" max = "8" class ="form-range" oninput="sliderMoved(this)" > < / div > {{next_button}} {{include_sibling 'chart.html'}} < script > function sliderMoved(input) { let idx = parseInt(input.value); let row = js_vars.rows[idx]; let to_self = row.to_self; let to_other = row.to_other; redrawChart(to_self, to_other); } < / script > {{include_sibling 'instructions.html'}} {{endblock}} dollar_auction / Bid.html From otree - more - demos {{block title}} Auction {{endblock}} {{block content}} < p id = "msg-my-status" > < / p > < p id = "msg-my-bid" > < / p > < button type = "button" id = "btn-bid" onclick = "sendBid(this)" > < / button > < br > < br > {{include_sibling 'instructions.html'}} < script > let bidBtn = document.getElementById('btn-bid'); let msgMyStatus = document.getElementById('msg-my-status'); let msgMyBid = document.getElementById('msg-my-bid'); function sendBid(btn) { liveSend(parseInt(btn.value)); } function liveRecv(data) { console.log('liveRecv', data) let am_top_bidder = data.top_bidder == = js_vars.my_id; let am_second_bidder = data.second_bidder == = js_vars.my_id; if (data.top_bid === 0) { msgMyStatus.innerText = 'Nobody has made a bid yet'; } else if (am_top_bidder) { msgMyStatus.innerText = 'You are the top bidder'; bidBtn.disabled = 'disabled'; msgMyBid.innerText = `Your bid is ${data.top_bid} points.`; } else { bidBtn.disabled = ''; if (am_second_bidder) { msgMyBid.innerText = `Your bid is ${data.second_bid} points.The top bid is ${data.top_bid} points (player ${data.top_bidder})`; msgMyStatus.innerText = 'You are the second bidder'; } else { msgMyBid.innerText = ''; msgMyStatus.innerText = 'You are not the top or second bidder.' } } let nextBid = data.top_bid + 10; bidBtn.value = nextBid; bidBtn.innerText = `Bid ${nextBid} points`; } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > {{endblock}} dollar_auction / instructions.html From otree - more - demos < h3 > Rules < / h3 > < p > In this auction, the jackpot of {{C.JACKPOT}} points goes to the highest bidder. The second highest bidder still has to pay the amount of their bid. < / p > shop / __init__.py From otree - more - demos from otree.api import * doc = """ Shopping app (online grocery store) """ def read_csv(): import csv f = open(__name__ + '/catalog.csv', encoding='utf-8-sig') rows = [row for row in csv.DictReader(f)] for row in rows: # all values in CSV are string unless you convert them row['unit_price'] = cu(row['unit_price']) row['image_path'] = 'grocery/{}.png'.format(row['image_png']) return rows class C(BaseConstants): NAME_IN_URL = 'shop' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 PRODUCTS = read_csv() # SKU = 'stock keeping unit' = product ID PRODUCTS_DICT = {row['sku']: row for row in PRODUCTS} class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): total_price = models.CurrencyField(initial=0) class Item(ExtraModel): player = models.Link(Player) sku = models.StringField() name = models.StringField() quantity = models.IntegerField() unit_price = models.CurrencyField() def total_price(item: Item): return item.quantity * item.unit_price def to_dict(item: Item): return dict( sku=item.sku, name=item.name, quantity=item.quantity, total_price=total_price(item), ) def live_method(player: Player, data): if 'sku' in data: sku = data['sku'] delta = data['delta'] product = C.PRODUCTS_DICT[sku] matches = Item.filter(player=player, sku=sku) if matches: [item] = matches item.quantity += delta if item.quantity <= 0: item.delete() else: if delta > 0: Item.create( player=player, quantity=delta, sku=sku, name=product['name'], unit_price=product['unit_price'], ) items = Item.filter(player=player) item_dicts = [to_dict(item) for item in items] player.total_price = sum([total_price(item) for item in items]) return {player.id_in_group: dict(items=item_dicts, total_price=player.total_price)} # PAGES class MyPage(Page): live_method = live_method class Results(Page): @staticmethod def vars_for_template(player: Player): return dict(items=Item.filter(player=player)) page_sequence = [MyPage, Results] shop / Results.html From otree - more - demos {{block title}} Thank you for your order {{endblock}} {{block content}} < p > Your order cost {{player.total_price}}. < / p > < table class ="table table-striped" > < tr > < th > Product < / th > < th > Quantity < / th > < / tr > {{ for i in items}} < tr > < td > {{i.name}} < / td > < td > {{i.quantity}} < / td > < / tr > {{endfor}} < / table > {{endblock}} shop / MyPage.html From otree - more - demos {{block title}} Welcome to oTree Grocery {{endblock}} {{block content}} < h3 > My cart < / h3 > < table class ="table table-striped" > < thead > < tr > < th > Name < / th > < th > Quantity < / th > < th > Total price < / th > < th > < / th > < / tr > < / thead > < tbody id = "cart-body" > < / tbody > < / table > < p > < button class ="btn btn-primary" > Checkout( < b > < span id = "cart-total" > < / span > < / b >) < / button > < script > let myCart = document.getElementById('cart-body'); let cartTotal = document.getElementById('cart-total'); function cu(amount) { return `${amount} points `; } function liveRecv(data) { let html = ''; for (let item of data.items) { html += ` < tr > < td > ${item.name} < / td > < td > ${item.quantity} < / td > < td > ${cu(item.total_price)} < / td > < td > < button type="button" value="${item.sku}" onclick="removeFromCart(this)" class ="btn btn-secondary" > -1 < / button > < / td > < / tr > `; } myCart.innerHTML = html; cartTotal.innerText = cu(data.total_price); } < / script > < h3 > Catalog < / h3 > < div class ="d-flex flex-wrap" > {{ for p in C.PRODUCTS}} < div class ="card" > < div class ="card-body" > < img src = "{{ static p.image_path }}" class ="card-img-top" style="width: 50px" > < h5 class ="card-title" > {{p.name}} < / h5 > < p class ="card-text" > Price: {{p.unit_price}} < / p > < button type = "button" value = "{{ p.sku }}" onclick = "addToCart(this)" class ="btn btn-secondary" > +1 < / button > < / div > < / div > {{endfor}} < / div > < script > function addToCart(ele) { modifyCart(ele.value, 1); } function removeFromCart(ele) { modifyCart(ele.value, -1); } function modifyCart(sku, delta) { liveSend({'sku': sku, 'delta': delta}); } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > < !-- icon attribution; remove this if you 're not using the grocery icons --> < br > < br > < br > < div style = "font-size: x-small;" > Icons made by < a style = "color: inherit;" href = "https://www.freepik.com" title = "Freepik" > Freepik < / a > from < a style = "color: inherit;" href = "https://www.flaticon.com/" title = "Flaticon" > www.flaticon.com < / a > < / div > {{endblock}} tictactoe / __init__.py From otree - more - demos from otree.api import * import json doc = """ Tic-tac-toe """ BLANK = ' ' class C(BaseConstants): NAME_IN_URL = 'tictactoe' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): winner = models.StringField(initial='') board_state = models.LongStringField(initial=BLANK * 9) whose_turn = models.StringField(initial='X') class Player(BasePlayer): is_winner = models.BooleanField() symbol = models.StringField() def creating_session(subsession: Subsession): for p in subsession.get_players(): p.symbol = {1: 'X', 2: 'O'}[p.id_in_group] def get_winning_symbol(board: list): winning_lines = [ # rows [0, 1, 2], [3, 4, 5], [6, 7, 8], # columns [0, 3, 6], [1, 4, 7], [2, 5, 8], # diagonals [0, 4, 8], [2, 4, 6], ] for entries in winning_lines: values = [board[coord] for coord in entries] if values in [['X', 'X', 'X'], ['O', 'O', 'O']]: return values[0] class Play(Page): @staticmethod def js_vars(player: Player): return dict(my_symbol=player.symbol) @staticmethod def live_method(player: Player, data: dict): group = player.group board = list(group.board_state) broadcast = {} if 'move' in data: move = data['move'] # if the game is already over if group.winner: return # you can't mark a square that was already marked if not board[move] == BLANK: return # you can't move out of turn if player.symbol != group.whose_turn: return group.whose_turn = player.get_others_in_group()[0].symbol board[move] = player.symbol group.board_state = ''.join(board) broadcast['board_state'] = board players = group.get_players() winning_symbol = get_winning_symbol(board) if winning_symbol: group.winner = winning_symbol for p in players: p.is_winner = p.symbol == winning_symbol broadcast['winning_symbol'] = winning_symbol elif BLANK in board: broadcast['whose_turn'] = group.whose_turn else: broadcast['draw'] = True return {0: broadcast} page_sequence = [Play] tictactoe / Play.html From otree - more - demos {{block content}} < style > # ttt-board td { width: 3 em; height: 3 em; text - align: center; font - size: x - large; border: 1 px solid black; } # ttt-board { width: auto; text - align: center; } < / style > < p > Your symbol is {{player.symbol}} < / p > < table id = "ttt-board" > < tr > < td id = "square0" data - square = "0" onclick = "sendMove(this)" > < / td > < td id = "square1" data - square = "1" onclick = "sendMove(this)" > < / td > < td id = "square2" data - square = "2" onclick = "sendMove(this)" > < / td > < / tr > < tr > < td id = "square3" data - square = "3" onclick = "sendMove(this)" > < / td > < td id = "square4" data - square = "4" onclick = "sendMove(this)" > < / td > < td id = "square5" data - square = "5" onclick = "sendMove(this)" > < / td > < / tr > < tr > < td id = "square6" data - square = "6" onclick = "sendMove(this)" > < / td > < td id = "square7" data - square = "7" onclick = "sendMove(this)" > < / td > < td id = "square8" data - square = "8" onclick = "sendMove(this)" > < / td > < / tr > < / table > < div id = "status" > < / div > < script > let my_symbol = js_vars.my_symbol; let finished = false; function writeStatus(msg) { document.getElementById('status').innerText = msg; } function liveRecv(data) { if (data.winning_symbol) { finished = true; if (data.winning_symbol === my_symbol) { writeStatus('You won!'); } else { writeStatus('You lost :('); } } if (data.draw) { writeStatus('Game ended in a draw'); } if (data.whose_turn) { if (data.whose_turn === my_symbol) { writeStatus("It's your turn"); } else { writeStatus("Waiting for the other player"); } } let boardState = data.board_state; for (let i = 0; i < boardState.length; i++) { document.getElementById(`square${i} `).innerText = {'X': '╳', 'O': '◯', ' ': ' '}[boardState[i]]; } } function sendMove(cell) { if (finished) return; liveSend({move: parseInt(cell.dataset.square)}); } document.addEventListener("DOMContentLoaded", function(event) { liveSend({}); }); < / script > \ {{endblock}}